summaryrefslogtreecommitdiff
path: root/planner/planner_generator.py
diff options
context:
space:
mode:
Diffstat (limited to 'planner/planner_generator.py')
-rwxr-xr-xplanner/planner_generator.py396
1 files changed, 396 insertions, 0 deletions
diff --git a/planner/planner_generator.py b/planner/planner_generator.py
new file mode 100755
index 0000000..f2c6408
--- /dev/null
+++ b/planner/planner_generator.py
@@ -0,0 +1,396 @@
+#!/usr/bin/env python3
+
+import argparse
+import calendar
+import datetime
+import locale
+import logging
+import os
+import shutil
+import subprocess
+import sys
+
+from typing import Optional
+
+try:
+ from lesana.command import _get_first_docstring_line # type: ignore
+except ImportError:
+ def _get_first_docstring_line(obj):
+ return ""
+
+try:
+ import argcomplete # type: ignore
+except ImportError:
+ argcomplete = False
+
+import jinja2
+
+
+locale.setlocale(locale.LC_ALL, '')
+
+
+class Generator:
+ """
+ """
+ default_template = "planner-A6"
+
+ def __init__(
+ self,
+ year: Optional[int] = None,
+ template: Optional[str] = None,
+ cover_template: Optional[str] = None,
+ out_file: Optional[str] = None,
+ build_dir: Optional[str] = "build",
+ latitude: Optional[float] = None,
+ longitude: Optional[float] = None,
+ timezone: Optional[str] = None,
+ ):
+ self.year = year or (
+ datetime.date.today() + datetime.timedelta(days=334)
+ ).year
+
+ self.latitude = latitude
+ self.longitude = longitude
+ self.timezone = timezone
+
+ self.out_file = out_file or (
+ template or self.default_template
+ ) + ".pdf"
+
+ self.paper_size = self._get_paper_size(template)
+
+ env = jinja2.Environment()
+ self.templates_dir = "templates"
+ loader = jinja2.FileSystemLoader(self.templates_dir)
+ if not template:
+ template = self.default_template
+ self.template_recto = loader.load(env, template + "-r.svg")
+ self.template_verso = loader.load(env, template + "-v.svg")
+ self.template_cover = loader.load(
+ env,
+ self._get_cover_name(cover_template)
+ )
+ self.build_dir = build_dir
+ self.page_fname = os.path.join(
+ self.build_dir,
+ template + "-{year}-{page:03}.svg"
+ )
+
+ def _get_paper_size(self, template):
+ if "A6" in template:
+ return "a6"
+ if "A5" in template:
+ return "a5"
+ if "A4" in template:
+ return "a4"
+ return "a6"
+
+ def _get_cover_name(self, cover_template):
+ if cover_template:
+ return cover_template
+ return "cover-{}-r.svg".format(self.paper_size.upper())
+
+ def render_page(self, page: int, **kw):
+ # page counts starts with 0
+ if page == 0:
+ template = self.template_cover
+ elif page % 2 == 0:
+ template = self.template_recto
+ else:
+ template = self.template_verso
+ with open(self.page_fname.format(
+ year=self.year, page=page
+ ), "w") as fp:
+ fp.write(template.render(**kw))
+
+ def run(self):
+ self.clean_build_dir()
+ self.generate_cover_page()
+ self.generate_pages()
+ self.convert_pages_to_svg()
+ self.join_pages()
+
+ def clean_build_dir(self):
+ try:
+ shutil.rmtree(self.build_dir)
+ except FileNotFoundError:
+ pass
+ os.makedirs(self.build_dir)
+
+ def generate_cover_page(self):
+ self.render_page(page=0, year=self.year)
+
+ def generate_pages(self):
+ pass
+
+ def convert_pages_to_svg(self):
+ inkscape_commands = ";\n".join([
+ (
+ "file-open:{build_dir}/{svg};"
+ + " export-type: pdf;"
+ + " export-filename:build/{pdf};"
+ + " export-text-to-path;"
+ + " export-do"
+ ).format(
+ build_dir=self.build_dir,
+ svg=s,
+ pdf=os.path.splitext(s)[0] + ".pdf",
+ )
+ for s in os.listdir(self.build_dir)
+ ])
+ try:
+ subprocess.run(
+ ["inkscape", "--shell"],
+ input=inkscape_commands,
+ text=True,
+ )
+ except FileNotFoundError:
+ logging.warning("Inkscape is not installed, can't convert to pdf")
+ logging.warning("Stopping here, you can use the svgs as you like")
+ sys.exit(1)
+
+ def get_pdf_pages(self):
+ pdf_pages = sorted([
+ os.path.join(self.build_dir, p)
+ for p in os.listdir(self.build_dir)
+ if p.endswith(".pdf")
+ ])
+ return pdf_pages
+
+ def join_pages(self):
+ pdf_pages = self.get_pdf_pages()
+ try:
+ subprocess.run([
+ "pdfjam",
+ "--outfile", self.out_file,
+ "--scale", "1",
+ "--paper", "{}paper".format(self.paper_size),
+ *pdf_pages
+ ])
+ except FileNotFoundError:
+ logging.warning("pdfjam is not installed")
+ logging.warning("you will have to join the pdf pages yourself")
+ sys.exit(1)
+
+
+class WeeklyGenerator(Generator):
+ """
+ """
+ default_template = "week_on_two_pages-A6"
+
+ def generate_pages(self):
+ cal = calendar.Calendar()
+ weeks = sum(
+ [r[0] for r in cal.yeardatescalendar(self.year, width=1)],
+ []
+ )
+
+ last_monday = None
+ page = 1
+ for week in weeks:
+ # yeardatescalendar will have the same week twice at the
+ # margin of a month, but we want to skip one of those
+ if week[0] == last_monday:
+ continue
+ last_monday = week[0]
+
+ self.render_page(page=page, week=week)
+ page += 1
+
+ self.render_page(page=page, week=week)
+ page += 1
+
+
+class DailyGenerator(Generator):
+ """
+ """
+ default_template = "daily-A6"
+
+ def generate_pages(self):
+ day = datetime.date(self.year, 1, 1)
+
+ # we want to start with a left side page (starting from 0)
+ page = 2
+ while day.year == self.year:
+ self.render_page(page=page, day=day)
+ page += 1
+ day += datetime.timedelta(days=1)
+
+ if day.year > self.year:
+ break
+
+ self.render_page(page=page, day=day)
+ page += 1
+ day += datetime.timedelta(days=1)
+
+ def get_pdf_pages(self):
+ pdf_pages = super().get_pdf_pages()
+ # insert an empty page on the second page, to start the year on
+ # a left page
+ pdf_pages.insert(1, "1, {}")
+
+ return pdf_pages
+
+
+class MonthGenerator(Generator):
+ """
+ """
+ default_template = "month-A6"
+
+ def generate_pages(self):
+ cal = calendar.Calendar()
+ full_year = cal.yeardatescalendar(self.year, width=1)
+ months = []
+
+ for i in range(12):
+ months.append([
+ day for week in full_year[i][0] for day in week
+ if day.month == i + 1
+ ])
+
+ texts = self.get_texts()
+
+ page = 2
+ for i, month in enumerate(months):
+ self.render_page(page=page, month=month, text=texts[i])
+ page += 1
+
+ def generate_cover_page(self):
+ pass
+
+ def get_texts(self):
+ return [[] for i in range(12)]
+
+
+class EphemerismonthGenerator(MonthGenerator):
+ """
+ """
+ default_template = "month-A6"
+
+ def get_texts(self):
+ # we import suntime just here, because it's a third party
+ # library and not used elsewhere
+ try:
+ import astral
+ except ImportError:
+ print("Printing a month planner with ephemeris requires"
+ "the astral library.")
+ sys.exit(1)
+
+ if not self.latitude or not self.longitude or not self.timezone:
+ print("Printing ephemeris requires latitude and longitude")
+ sys.exit(1)
+
+ location = astral.Location((
+ "",
+ "",
+ self.latitude,
+ self.longitude,
+ self.timezone,
+ 0
+ ))
+
+ day = datetime.date(self.year, 1, 1)
+
+ texts = []
+ while day.year == self.year:
+ month = []
+ this_month = day.month
+ while day.month == this_month:
+ sunrise = location.sunrise(day)
+ noon = location.solar_noon(day)
+ sunset = location.sunset(day)
+ moon_phase = location.moon_phase(day)
+ if moon_phase < 7:
+ moon_icon = "●"
+ elif moon_phase < 14:
+ moon_icon = "☽"
+ elif moon_phase < 21:
+ moon_icon = "○"
+ else:
+ moon_icon = "☾"
+ text = ("☼ {sunrise} — {noon} — {sunset} "
+ + "{moon_icon} {moon_phase}").format(
+ sunrise=sunrise.strftime("%H:%M"),
+ noon=noon.strftime("%H:%M"),
+ sunset=sunset.strftime("%H:%M"),
+ moon_icon=moon_icon,
+ moon_phase=moon_phase,
+ )
+ month.append(text)
+ day += datetime.timedelta(days=1)
+ texts.append(month)
+
+ return texts
+
+
+class Command:
+ """
+ Generate a planner
+ """
+ def get_parser(self):
+ desc = _get_first_docstring_line(self)
+ parser = argparse.ArgumentParser(description=desc)
+ parser.add_argument(
+ "--year", '-y',
+ default=None,
+ help="Default is next year, or this year in January."
+ )
+ parser.add_argument(
+ "--template", '-t',
+ default=None,
+ help="Base name of the template (without -[rv].svg)",
+ )
+ parser.add_argument(
+ "--cover-template",
+ default=None,
+ help="Full name of the template (including -[rv].svg)",
+ )
+ parser.add_argument(
+ "--latitude",
+ default=None,
+ type=float,
+ help="Latitude for ephemeris calculation",
+ )
+ parser.add_argument(
+ "--longitude",
+ default=None,
+ type=float,
+ help="Longitude for ephemeris calculation",
+ )
+ parser.add_argument(
+ "--timezone",
+ default=None,
+ help="Timezone for ephemeris calculation (e.g. Europe/Rome)",
+ )
+ parser.add_argument(
+ "command",
+ )
+ return parser
+
+ def main(self):
+ self.parser = self.get_parser()
+ if argcomplete:
+ argcomplete.autocomplete(self.parser)
+ self.args = self.parser.parse_args()
+
+ generator = getattr(
+ sys.modules[__name__],
+ self.args.command.capitalize() + "Generator",
+ None
+ )
+ if generator:
+ generator(
+ year=self.args.year,
+ template=self.args.template,
+ cover_template=self.args.cover_template,
+ latitude=self.args.latitude,
+ longitude=self.args.longitude,
+ timezone=self.args.timezone,
+ ).run()
+ else:
+ print("command not supported: {}".format(self.args.command))
+
+
+if __name__ == "__main__":
+ Command().main()