From 44d1c0b25a5d3acb8c605b4d00b72ac794931238 Mon Sep 17 00:00:00 2001 From: "geortz@gmail.com" Date: Sun, 29 Dec 2013 20:56:48 +0000 Subject: [PATCH 02/17] holiday support holiday definitions for Greece and France TR language git-svn-id: https://callirhoe.googlecode.com/svn/branches/next@21 81c8bb96-aa45-f2e2-0eef-c4fa4a15c6df --- callirhoe.py | 35 ++--- holidays/french_holidays.EN.dat | 23 ++++ holidays/french_holidays.FR.dat | 23 ++++ holidays/greek_holidays.EL.dat | 31 +++++ holidays/greek_namedays.EL.dat | 55 ++++++++ lang/TR.py | 31 +++++ layouts/classic.py | 30 +++-- lib/holiday.py | 231 ++++++++++++++++++++++++++++++++ style/bw.py | 2 +- style/default.py | 6 +- style/gfs.py | 2 +- style/rainbow-gfs.py | 2 +- style/rainbow.py | 2 +- 13 files changed, 439 insertions(+), 34 deletions(-) create mode 100644 holidays/french_holidays.EN.dat create mode 100644 holidays/french_holidays.FR.dat create mode 100644 holidays/greek_holidays.EL.dat create mode 100644 holidays/greek_namedays.EL.dat create mode 100644 lang/TR.py create mode 100644 lib/holiday.py diff --git a/callirhoe.py b/callirhoe.py index 2c1e3c0..2a7755f 100755 --- a/callirhoe.py +++ b/callirhoe.py @@ -22,12 +22,12 @@ # allow to change background color (fill), other than white # page spec parse errors # mobile themes (e.g. 800x480) -# photo support -# implement DATA SOURCES +# photo support (like ImageMagick's polaroid effect) # python source documentation # .callirhoe/config : default values for plugins (styles/layouts/lang...) and cmdline # MAYBE-TODO: +# implement various data sources # auto-landscape? should aim for matrix or bars? # allow /usr/bin/date-like formatting %x... # improve file matching with __init__ when lang known @@ -38,13 +38,14 @@ # CANNOT UPGRADE TO argparse !!! -- how to handle [[month] year] form? -_version = "0.2.1.r15" +_version = "0.2.2" import calendar import sys import time import optparse import lib.xcairo as xcairo +import lib.holiday as holiday from lib.plugin import * # TODO: SEE IF IT CAN BE MOVED INTO lib.plugin ... @@ -75,7 +76,7 @@ parser.add_option("-l", "--lang", dest="lang", default="EN", help="choose language [%default]") parser.add_option("-t", "--layout", dest="layout", default="classic", help="choose layout [%default]") -parser.add_option("-H", "--layout-help", dest="layouthelp", action="store_true", default=False, +parser.add_option("-?", "--layout-help", dest="layouthelp", action="store_true", default=False, help="show layout-specific help") parser.add_option("--examples", dest="examples", action="store_true", help="display some usage examples") @@ -93,6 +94,8 @@ parser.add_option("--paper", default="a4", "-W or -H pixels; 'w' suffix swaps width & height [%default]") parser.add_option("--border", type="float", default=3, help="set border size (in mm) [%default]") +parser.add_option("-H", "--with-holidays", action="append", dest="holidays", + help="load holiday file (can be used multiple times)") def print_examples(): print """Examples: @@ -100,9 +103,12 @@ def print_examples(): Create a calendar of the current year (by default in a 4x3 grid): $ callirhoe my_calendar.pdf -Same as above, but in landscape mode (3x4): +Same as above, but in landscape mode (3x4) (for printing): $ callirhoe --landscape my_calendar.pdf +Landscape via rotation (for screen): + $ callirhoe --paper=a4w --rows=3 my_calendar.pdf + Forcing 1 row only, we get month bars instead of boxes: $ callirhoe --landscape --rows=1 my_calendar.pdf @@ -199,15 +205,6 @@ if options.geom_assign: calendar.month_name = Language.month_name calendar.day_name = Language.day_name -def get_orthodox_easter(y): - y1, y2, y3 = y - y//4 * 4, y - y//7 * 7, y - y//19 * 19 - a = 19*y3 + 15 - y4 = a - a//30 * 30 - b = 2*y1 + 4*y2 + 6*(y4 + 1) - y5 = b - b/7 * 7 - r = 1 + 3 + y4 + y5; - return (5, r - 30) if r > 30 else (4,r) - def itoa(s): try: k = int(s); @@ -258,4 +255,12 @@ xcairo.XDPI = options.dpi Geometry.pagespec = options.paper Geometry.border = options.border -Layout.draw_calendar(Outfile, Year, Month, MonthSpan, (Style,Geometry), _version) +hp = holiday.HolidayProvider(Style.dom, Style.dom_weekend, + Style.dom_holiday, Style.dom_weekend_holiday) + +print "Holidays: ", options.holidays +#hp.load_holiday_file("./holidays/greek_holidays.EL.dat") +#hp.load_holiday_file("./holidays/greek_namedays.EL.dat") +#hp.load_holiday_file("./holidays/french_holidays.FR.dat") + +Layout.draw_calendar(Outfile, Year, Month, MonthSpan, (Style,Geometry), hp, _version) diff --git a/holidays/french_holidays.EN.dat b/holidays/french_holidays.EN.dat new file mode 100644 index 0000000..beae635 --- /dev/null +++ b/holidays/french_holidays.EN.dat @@ -0,0 +1,23 @@ +# a: holiday occurs annually fixed day/month +# m: holiday occurs monthly, fixed day +# f: fixed day/month/year combination (e.g. deadline, trip, etc.) +# oe: Orthodox Easter-dependent holiday, annually +# ge: Georgios' name day, Orthodox Easter dependent holiday, annually +# ce: Catholic Easter holiday + +# type|day|month|year|footer|header|off + +a|1|1|||New year's|off +a|1|5|||Labour day|off +a|8|5|||Victory in Europe|off +a|14|7|||Bastille|off +a|15|8|||Assumption of Mary|off +a|1|11|||All Saints|off +a|11|11|||Armistice|off +a|25|12|||Christmas|off +a|26|12|||St. Stephen's|off +ce|-2||||Good Friday| +ce|0||||Easter|off +ce|1||||Easter Monday|off +ce|39||||Ascension|off +ce|50||||Whit Monday|off diff --git a/holidays/french_holidays.FR.dat b/holidays/french_holidays.FR.dat new file mode 100644 index 0000000..182cb7e --- /dev/null +++ b/holidays/french_holidays.FR.dat @@ -0,0 +1,23 @@ +# a: holiday occurs annually fixed day/month +# m: holiday occurs monthly, fixed day +# f: fixed day/month/year combination (e.g. deadline, trip, etc.) +# oe: Orthodox Easter-dependent holiday, annually +# ge: Georgios' name day, Orthodox Easter dependent holiday, annually +# ce: Catholic Easter holiday + +# type|day|month|year|footer|header|off + +a|1|1|||Nouvel an|off +a|1|5|||Fête du Travail|off +a|8|5|||Fête de la Victoire|off +a|14|7|||Fête nationale|off +a|15|8|||Assomption|off +a|1|11|||Toussaint|off +a|11|11|||Armistice de 1918|off +a|25|12|||Noël|off +a|26|12|||Saint-Étienne|off +ce|-2||||Vendredi saint| +ce|0||||Pâques|off +ce|1||||Lundi de Pâques|off +ce|39||||Ascension|off +ce|50||||Lundi de Pentecôte|off diff --git a/holidays/greek_holidays.EL.dat b/holidays/greek_holidays.EL.dat new file mode 100644 index 0000000..7557766 --- /dev/null +++ b/holidays/greek_holidays.EL.dat @@ -0,0 +1,31 @@ +# a: holiday occurs annually fixed day/month +# m: holiday occurs monthly, fixed day +# f: fixed day/month/year combination (e.g. deadline, trip, etc.) +# oe: Orthodox Easter-dependent holiday, annually +# ge: Georgios' name day, Orthodox Easter dependent holiday, annually +# ce: Catholic Easter holiday + +# type|day|month|year|footer|header|off + +a|1|1|||ΠΡΩΤΟΧΡΟΝΙΑ|off +a|6|1|||ΘΕΟΦΑΝΕΙΑ|off +a|25|3|||ΕΥΑΓΓΕΛΙΣΜΟΣ|off +a|1|5|||ΠΡΩΤΟΜΑΓΙΑ|off +a|15|8|||ΚΟΙΜΗΣΗ ΘΕΟΤΟΚΟΥ|off +a|28|10|||ΕΠΕΤΕΙΟΣ ΤΟΥ «ΟΧΙ»|off +a|17|11|||ΠΟΛΥΤΕΧΝΕΙΟ| +a|25|12|||ΧΡΙΣΤΟΥΓΕΝΝΑ|off +a|26|12|||2η ΜΕΡΑ ΧΡΙΣΤΟΥΓ.|off +oe|-70||||Αρχή Τριωδίου|off +oe|-59||||Τσικνοπέμπτη| +oe|-56||||Της Απόκρεω| +oe|-49||||Της Τυροφάγου|off +oe|-48||||ΚΑΘΑΡΑ ΔΕΥΤΕΡΑ|off +oe|-8||||Σαβ. Λαζάρου| +oe|-7||||Κυρ. Βαϊων| +oe|-2||||ΜΕΓ. ΠΑΡΑΣΚΕΥΗ|off +oe|0||||ΑΓΙΟ ΠΑΣΧΑ|off +oe|1||||2η ΜΕΡΑ ΠΑΣΧΑ|off +oe|49||||ΠΕΝΤΗΚΟΣΤΗ| +oe|50||||ΑΓΙΟΥ ΠΝΕΥΜΑΤΟΣ|off +ce|0||||Καθολικό Πάσχα| diff --git a/holidays/greek_namedays.EL.dat b/holidays/greek_namedays.EL.dat new file mode 100644 index 0000000..9168185 --- /dev/null +++ b/holidays/greek_namedays.EL.dat @@ -0,0 +1,55 @@ +a|1|1||Βασίλης|| +a|6|1||Φώτης, Φάνης, Ιορδάνης|| +a|7|1||Ιωάννης Πρόδρομος|| +a|17|1||Αντώνης, Γιώργος|| +a|18|1||Αθανάσιος, Κύριλλος|| +a|20|1||Ευθύμιος|| +a|25|1||Γρηγόρης|| +a|1|2||Τρύφων, Αναστάσιος|| +a|7|2||Λουκάς|| +a|10|2||Χαράλαμπος, Χαρίκλεια, Ζήνων|| +a|14|2||Βαλεντίνος|| +a|25|3||Ευάγγελος|| +ge||||Γιώργος|| +a|7|5||Ειρήνη|| +a|9|5||Χριστόφορος|| +a|21|5||Κωνσταντίνος, Ελένη|| +a|30|6||Απόστολος|| +a|1|7||Κοσμάς, Διαμιανός, Ανάργυρος|| +a|11|7||Όλγα, Ευφημία|| +a|17|7||Μαρίνα|| +a|20|7||Ηλίας|| +a|26|7||Παρασκευή|| +a|27|7||Παντελεήμων|| +a|6|8||Σωτήρης|| +a|15|8||Παναγιώτης, Μαρία, Δέσποινα|| +a|30|8||Αλέξανδρος|| +a|14|9||Σταύρος|| +a|17|9||Σοφία, Αγάπη, Ελπίδα|| +a|20|9||Ευστάθιος|| +a|26|10||Δημήτριος|| +a|1|11||Κοσμάς, Δαμιανός, Ανάργυρος|| +a|8|11||Άγγελος, Μιχ, Γαβρ, Ταξιάρ.|| +a|9|11||Νεκτάριος|| +a|10|11||Ορέστης|| +a|14|11||Φίλιππος|| +a|21|11||Εισόδια Θεοτόκου (Μαρία)|| +a|25|11||Αικατερίνη, Μερκούριος|| +a|26|11||Στυλιανός|| +a|30|11||Ανδρέας|| +a|4|12||Βαρβάρα|| +a|5|12||Σάββας, Διογένης|| +a|6|12||Νικόλαος|| +a|9|12||Άννα|| +a|12|12||Σπυρίδων|| +a|15|12||Ελευθέριος, Ελευθερία|| +a|22|12||Αναστασία|| +a|24|12||Ευγενία|| +a|25|12||Χρήστος, Χριστίνα|| +a|27|12||Στέφανος|| +e|-43||||Αγίων Θεοδώρων| +e|0|||Αναστάσιος, Πασχάλης, Λάμπρος|| +e|-8|||Λάζαρος|| +e|-7|||Βάιος|| +e|7|||Θωμάς|| +e|56||||Αγίων Πάντων| diff --git a/lang/TR.py b/lang/TR.py new file mode 100644 index 0000000..cf82b4e --- /dev/null +++ b/lang/TR.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- + +# callirhoe - high quality calendar rendering +# Copyright (C) 2012 George M. Tzoumas + +# Turkish language data +# Copyright (C) 2013 Ece Neslihan Aybeke + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see http://www.gnu.org/licenses/ + +day_name = [ u'Pazartesi', u'Salı', u' Çarşamba ', + u'Perşembe', u'Cuma', u'Cumartesi', u'Pazar' ] + +short_day_name = [ u'Pt', u'Sa', u'Ça', u'Pe', u'Cu', u'Ct', u'Pa' ] + +month_name = [ '', u'Ocak', 'Şubat', u'Mart', u'Nisan', + u'Mayıs', u'Haziran', u'Temmuz', u'Ağustos', + u'Eylül', u'Ekim', u'Kasım', u'Aralık' ] + +short_month_name = month_name diff --git a/layouts/classic.py b/layouts/classic.py index ecf1a25..30ff102 100644 --- a/layouts/classic.py +++ b/layouts/classic.py @@ -23,6 +23,7 @@ from math import floor, ceil, sqrt import calendar import optparse import sys +from datetime import date, timedelta parser = optparse.OptionParser(usage="%prog (...) --layout classic [options] (...)",add_help_option=False) parser.add_option("--rows", type="int", default=0, help="force grid rows [%default]") @@ -139,9 +140,8 @@ def draw_day_cell(cr, rect, day, header, footer, theme, show_day_name, short_thr _draw_day_cell_short(cr, rect, day, header, footer, theme, show_day_name) else: _draw_day_cell_long(cr, rect, day, header, footer, theme, show_day_name) - -def draw_month_matrix(cr, rect, month, year, theme, daycell_thres): +def draw_month_matrix(cr, rect, month, year, theme, holiday_provider, daycell_thres): S,G = theme apply_rect(cr, rect, G.month.sloppy_dx, G.month.sloppy_dy, G.month.sloppy_rot) @@ -176,13 +176,15 @@ def draw_month_matrix(cr, rect, month, year, theme, daycell_thres): # draw day cells for row in range(weekrows): for col in range(7): - day_style = S.dom_weekend if col >= 5 else S.dom R = dom_grid.item(row, col) if dom > 0 and dom <= span: - draw_day_cell(cr, rect = R, day = (dom, col), - header = None, footer = None, theme = (day_style, G.dom), show_day_name = False, + holiday_tuple = holiday_provider(year, month, dom, col) + day_style = holiday_tuple[2] + draw_day_cell(cr, rect = R, day = (dom, col), + header = holiday_tuple[0], footer = holiday_tuple[1], theme = (day_style, G.dom), show_day_name = False, short_thres = daycell_thres) else: + day_style = S.dom_weekend if col >= 5 else S.dom draw_box(cr, rect = R, stroke_rgba = day_style.frame, fill_rgba = day_style.bg, stroke_width = mm_to_dots(day_style.frame_thickness)) dom += 1 @@ -205,7 +207,7 @@ def draw_month_matrix(cr, rect, month, year, theme, daycell_thres): align = (2,0), font = S.month.font, measure = mmeasure, shadow = mshad) cr.restore() -def draw_month_bar(cr, rect, month, year, theme, daycell_thres): +def draw_month_bar(cr, rect, month, year, theme, holiday_provider, daycell_thres): S,G = theme apply_rect(cr, rect, G.month.sloppy_dx, G.month.sloppy_dy, G.month.sloppy_rot) @@ -225,12 +227,14 @@ def draw_month_bar(cr, rect, month, year, theme, daycell_thres): # draw day cells for dom in range(1,rows+1): - day_style = S.dom_weekend if day >= 5 and dom <= span else S.dom R = dom_grid.item(dom-1) if dom <= span: - draw_day_cell(cr, rect = R, day = (dom, day), header = None, footer = None, + holiday_tuple = holiday_provider(year, month, dom, day) + day_style = holiday_tuple[2] + draw_day_cell(cr, rect = R, day = (dom, day), header = holiday_tuple[0], footer = holiday_tuple[1], theme = (day_style, G.dom), show_day_name = True, short_thres = daycell_thres) else: + day_style = S.dom draw_box(cr, rect = R, stroke_rgba = day_style.frame, fill_rgba = day_style.bg, stroke_width = mm_to_dots(day_style.frame_thickness)) day = (day + 1) % 7 @@ -252,11 +256,11 @@ def draw_month_bar(cr, rect, month, year, theme, daycell_thres): align = (2,0), font = S.month.font, measure = mmeasure, shadow = mshad) cr.restore() -def draw_month(cr, rect, month, year, theme, bar_thres = 0.7, daycell_thres = 2.5): +def draw_month(cr, rect, month, year, theme, holiday_provider, bar_thres = 0.7, daycell_thres = 2.5): if rect_ratio(rect) >= bar_thres: - draw_month_matrix(cr, rect, month, year, theme, daycell_thres) + draw_month_matrix(cr, rect, month, year, theme, holiday_provider, daycell_thres) else: - draw_month_bar(cr, rect, month, year, theme, daycell_thres) + draw_month_bar(cr, rect, month, year, theme, holiday_provider, daycell_thres) #1 1 1 #2 2 1 @@ -276,7 +280,7 @@ def draw_month(cr, rect, month, year, theme, bar_thres = 0.7, daycell_thres = 2. #rows = 0 #cols = 0 -def draw_calendar(Outfile, Year, Month, MonthSpan, Theme, version_string): +def draw_calendar(Outfile, Year, Month, MonthSpan, Theme, holiday_provider, version_string): S,G = Theme rows, cols = options.rows, options.cols @@ -353,7 +357,7 @@ def draw_calendar(Outfile, Year, Month, MonthSpan, Theme, version_string): for (m,y) in p: k = len(p) - num_placed - 1 if z_order == "decreasing" else num_placed draw_month(page.cr, grid.item_seq(k, options.grid_order == "column"), - month=m, year=y, theme = Theme, + month=m, year=y, theme = Theme, holiday_provider = holiday_provider, bar_thres = options.month_bar_ratio, daycell_thres = options.short_daycell_ratio) num_placed += 1 if (y > yy[-1]): yy.append(y) diff --git a/lib/holiday.py b/lib/holiday.py new file mode 100644 index 0000000..6f4e1c9 --- /dev/null +++ b/lib/holiday.py @@ -0,0 +1,231 @@ +# -*- coding: utf-8 -*- + +# callirhoe - high quality calendar rendering +# Copyright (C) 2012-2013 George M. Tzoumas + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see http://www.gnu.org/licenses/ + +# ***************************************** +# # +# holiday support routines # +# # +# ***************************************** + +from datetime import date, timedelta + +def _get_orthodox_easter(year): + """compute date of orthodox easter""" + y1, y2, y3 = year % 4 , year % 7, year % 19 + a = 19*y3 + 15 + y4 = a % 30 + b = 2*y1 + 4*y2 + 6*(y4 + 1) + y5 = b % 7 + r = 1 + 3 + y4 + y5 + return date(year, 3, 31) + timedelta(r) +# res = date(year, 5, r - 30) if r > 30 else date(year, 4, r) +# return res + +def _get_catholic_easter(year): + """compute date of catholic easter""" + a, b, c = year % 19, year // 100, year % 100 + d, e = divmod(b,4) + f = (b + 8) // 25 + g = (b - f + 1) // 3 + h = (19*a + b - d - g + 15) % 30 + i, k = divmod(c,4) + l = (32 + 2*e + 2*i - h - k) % 7 + m = (a + 11*h + 22*l) // 451 + emonth,edate = divmod(h + l - 7*m + 114,31) + return date(year, emonth, edate+1) + +class Holiday(object): + """class holding a Holiday object (date is not stored!) + + Properties: + header: string for header + footer: string for footer + flags : bit combination of {OFF=1} + OFF: day off (real holiday) + """ + OFF = 1 + def __init__(self, header = [], footer = [], flags_str = None): + self.header_list = self._strip_empty(header) + self.footer_list = self._strip_empty(footer) + self.flags = self._parse_flags(flags_str) + + def merge_with(self, hol_list): + for hol in hol_list: + self.header_list.extend(hol.header_list) + self.footer_list.extend(hol.footer_list) + self.flags |= hol.flags + + def header(self): + return self._flatten(self.header_list) + + def footer(self): + return self._flatten(self.footer_list) + + def __repr__(self): + return str(self.footer()) + ':' + str(self.header()) + ':' + str(self.flags) + + def _parse_flags(self, fstr): + if not fstr: return 0 + fs = fstr.split(',') + val = 0 + for s in fs: + if s == 'off': val |= Holiday.OFF + return val + + def _strip_empty(self, sl): + return filter(lambda z: z, sl) if sl else [] + + def _flatten(self, sl): + if not sl: return None + res = sl[0] + for s in sl[1:]: + res += ', ' + s + return res + +class HolidayProvider(object): + def __init__(self, s_normal, s_weekend, s_holiday, s_weekend_holiday): + self.annual = dict() # key = (d,m) + self.monthly = dict() # key = d + self.fixed = dict() # key = date() + self.orth_easter = dict() # key = daysdelta + self.george = [] # key = n/a + self.cath_easter = dict() # key = daysdelta + self.cache = dict() # key = date() + self.ycache = set() # key = year + self.s_normal = s_normal + self.s_weekend = s_weekend + self.s_holiday = s_holiday + self.s_weekend_holiday = s_weekend_holiday + + def parse_day_record(self, fields): + if len(fields) != 7: + raise ValueError("Too many fields: " + str(fields)) + for i in range(len(fields)): + if len(fields[i]) == 0: fields[i] = None + d = int(fields[1]) if fields[1] else 0 + m = int(fields[2]) if fields[2] else 0 + y = int(fields[3]) if fields[3] else 0 + return (fields[0],d,m,y,fields[4],fields[5],fields[6]) + + # a: holiday occurs annually fixed day/month + # m: holiday occurs monthly, fixed day + # f: fixed day/month/year combination (e.g. deadline, trip, etc.) + # oe: Orthodox Easter-dependent holiday, annually + # ge: Georgios' name day, Orthodox Easter dependent holiday, annually + # ce: Catholic Easter holiday + def load_holiday_file(self, filename): + with open(filename, 'r') as f: + for line in f: + if line[0] == '#': continue + line = line.rstrip() + if not line: continue + fields = line.split('|') + etype,d,m,y,footer,header,flags = self.parse_day_record(fields) + hol = Holiday([header], [footer], flags) + if etype == 'a': + if (d,m) not in self.annual: self.annual[(d,m)] = [] + self.annual[(d,m)].append(hol) + elif etype == 'm': + if d not in self.monthly: self.monthly[d] = [] + self.monthly[d].append(hol) + elif etype == 'f': + if date(y,m,d) not in self.fixed: self.fixed[date(y,m,d)] = [] + self.fixed[date(y,m,d)].append(hol) + elif etype == 'oe': + if d not in self.orth_easter: self.orth_easter[d] = [] + self.orth_easter[d].append(hol) + elif etype == 'ge': + self.george.append(hol) + elif etype == 'ce': + if d not in self.cath_easter: self.cath_easter[d] = [] + self.cath_easter[d].append(hol) + + def get_holiday(self, y, m, d): + if y not in self.ycache: + # fill-in events for year y + # annual + for d0,m0 in self.annual: + dt = date(y,m0,d0) + if not dt in self.cache: self.cache[dt] = Holiday() + self.cache[dt].merge_with(self.annual[(d0,m0)]) + # monthly + for d0 in self.monthly: + for m0 in range(1,13): + dt = date(y,m0,d0) + if not dt in self.cache: self.cache[dt] = Holiday() + self.cache[dt].merge_with(self.monthly[m0]) + # fixed + for dt in filter(lambda z: z.year == y, self.fixed): + if not dt in self.cache: self.cache[dt] = Holiday() + self.cache[dt].merge_with(self.fixed[dt]) + # orthodox easter + edt = _get_orthodox_easter(y) + for delta in self.orth_easter: + dt = edt + timedelta(delta) + if not dt in self.cache: self.cache[dt] = Holiday() + self.cache[dt].merge_with(self.orth_easter[delta]) + # Georgios day + if self.george: + dt = date(y,4,23) + if edt >= dt: dt = edt + timedelta(1) # >= or > ?? + if not dt in self.cache: self.cache[dt] = Holiday() + self.cache[dt].merge_with(self.george) + # catholic easter + edt = _get_catholic_easter(y) + for delta in self.cath_easter: + dt = edt + timedelta(delta) + if not dt in self.cache: self.cache[dt] = Holiday() + self.cache[dt].merge_with(self.cath_easter[delta]) + + self.ycache.add(y) + + dt = date(y,m,d) + return self.cache[dt] if dt in self.cache else None + + def __call__(self, year, month, dom, dow): + """returns (header,footer,day_style) + + Args: + month: month (0-12) + dom: day of month (1-31) + dow: day of week (0-6) + """ + hol = self.get_holiday(year,month,dom) + if hol: + if hol.flags & Holiday.OFF: + style = self.s_weekend_holiday if dow >= 5 else self.s_holiday + else: + style = self.s_weekend if dow >= 5 else self.s_normal + return (hol.header(),hol.footer(),style) + else: + return (None,None,self.s_weekend if dow >= 5 else self.s_normal) + +if __name__ == '__main__': + import sys + hp = HolidayProvider('n', 'w', 'h', 'wh') + y = int(sys.argv[1]) + for f in sys.argv[2:]: + hp.load_holiday_file(f) + if y == 0: y = date.today().year + cur = date(y,1,1) + d2 = date(y,12,31) + while cur <= d2: + y,m,d = cur.year, cur.month, cur.day + hol = hp.get_holiday(y,m,d) + if hol: print cur.strftime("%a %b %d %Y"),hol + cur += timedelta(1) diff --git a/style/bw.py b/style/bw.py index 98b06dd..bba007e 100644 --- a/style/bw.py +++ b/style/bw.py @@ -44,7 +44,7 @@ class dom_holiday(dom): header = (0,0,0) font = ("Times New Roman", 0, 1) -class dom_weekend_holiday_style(dom_holiday): +class dom_weekend_holiday(dom_holiday): bg = (0,0,0) class month: diff --git a/style/default.py b/style/default.py index e16b48d..b316d56 100644 --- a/style/default.py +++ b/style/default.py @@ -39,11 +39,13 @@ class dom_weekend(dom): fg = (0,0,1) class dom_holiday(dom): + bg = (0.7,1,1) fg = (1,0,0) header = (1,0,0) -class dom_weekend_holiday_style(dom_holiday): - bg = (0.7,1,1) +class dom_weekend_holiday(dom_holiday): + pass + #bg = (0.7,1,1) class month: font = ("Times New Roman", 0, 1) diff --git a/style/gfs.py b/style/gfs.py index d3f1d48..e0362a3 100644 --- a/style/gfs.py +++ b/style/gfs.py @@ -36,7 +36,7 @@ class dom_holiday(default.dom_holiday): font = ("GFS Bodoni",) header_font = footer_font = "GFS Elpis" -class dom_weekend_holiday_style(default.dom_weekend_holiday_style): +class dom_weekend_holiday(default.dom_weekend_holiday): font = "GFS Bodoni" header_font = footer_font = "GFS Elpis" diff --git a/style/rainbow-gfs.py b/style/rainbow-gfs.py index bc6598c..b0fa2ad 100644 --- a/style/rainbow-gfs.py +++ b/style/rainbow-gfs.py @@ -29,7 +29,7 @@ class dom_weekend(gfs.dom_weekend): pass class dom_holiday(gfs.dom_holiday): pass -class dom_weekend_holiday_style(gfs.dom_weekend_holiday_style): pass +class dom_weekend_holiday(gfs.dom_weekend_holiday): pass from lib.geom import color_mix, color_scale, color_auto_fg diff --git a/style/rainbow.py b/style/rainbow.py index dae9d35..cb51165 100644 --- a/style/rainbow.py +++ b/style/rainbow.py @@ -29,7 +29,7 @@ class dom_weekend(default.dom_weekend): pass class dom_holiday(default.dom_holiday): pass -class dom_weekend_holiday_style(default.dom_weekend_holiday_style): pass +class dom_weekend_holiday(default.dom_weekend_holiday): pass from lib.geom import color_mix, color_scale, color_auto_fg From dbdb9497d6bb7a131be81c86d29d9b7e9c713e2f Mon Sep 17 00:00:00 2001 From: "geortz@gmail.com" Date: Sun, 29 Dec 2013 23:56:21 +0000 Subject: [PATCH 03/17] support for multi-day events git-svn-id: https://callirhoe.googlecode.com/svn/branches/next@22 81c8bb96-aa45-f2e2-0eef-c4fa4a15c6df --- callirhoe.py | 13 ++++--- holidays/french_holidays.EN.dat | 6 +++- holidays/french_holidays.FR.dat | 6 +++- holidays/greek_holidays.EL.dat | 3 +- holidays/greek_namedays.EL.dat | 9 +++++ lib/holiday.py | 62 +++++++++++++++++++++++++-------- style/bw.py | 10 +++++- style/default.py | 8 ++++- style/gfs.py | 11 ++++-- style/rainbow-gfs.py | 6 +++- style/rainbow.py | 6 +++- 11 files changed, 112 insertions(+), 28 deletions(-) diff --git a/callirhoe.py b/callirhoe.py index 2a7755f..3205053 100755 --- a/callirhoe.py +++ b/callirhoe.py @@ -19,6 +19,9 @@ # TODO: +# fix overlapping multi-day events: +# use 'EVENT..', '..EVENT..', '..EVENT' formatting or sth. similar + # allow to change background color (fill), other than white # page spec parse errors # mobile themes (e.g. 800x480) @@ -173,7 +176,7 @@ Layout = import_plugin("layouts", "layout", "layouts", "--list-layouts", options for x in argv2: if '=' in x: x = x[0:x.find('=')] if not Layout.parser.has_option(x): - parser.error("invalid option %s; use --help (-h) or --layout-help (-H) to see available options" % x) + parser.error("invalid option %s; use --help (-h) or --layout-help (-?) to see available options" % x) (Layout.options,largs) = Layout.parser.parse_args(argv2) if options.layouthelp: @@ -256,11 +259,11 @@ Geometry.pagespec = options.paper Geometry.border = options.border hp = holiday.HolidayProvider(Style.dom, Style.dom_weekend, - Style.dom_holiday, Style.dom_weekend_holiday) + Style.dom_holiday, Style.dom_weekend_holiday, + Style.dom_multi, Style.dom_weekend_multi) print "Holidays: ", options.holidays -#hp.load_holiday_file("./holidays/greek_holidays.EL.dat") -#hp.load_holiday_file("./holidays/greek_namedays.EL.dat") -#hp.load_holiday_file("./holidays/french_holidays.FR.dat") +for f in options.holidays: + hp.load_holiday_file(f) Layout.draw_calendar(Outfile, Year, Month, MonthSpan, (Style,Geometry), hp, _version) diff --git a/holidays/french_holidays.EN.dat b/holidays/french_holidays.EN.dat index beae635..6ce81fe 100644 --- a/holidays/french_holidays.EN.dat +++ b/holidays/french_holidays.EN.dat @@ -5,7 +5,8 @@ # ge: Georgios' name day, Orthodox Easter dependent holiday, annually # ce: Catholic Easter holiday -# type|day|month|year|footer|header|off +# type|day*span|month|year|footer|header|off +# day*span supported only for f a|1|1|||New year's|off a|1|5|||Labour day|off @@ -21,3 +22,6 @@ ce|0||||Easter|off ce|1||||Easter Monday|off ce|39||||Ascension|off ce|50||||Whit Monday|off + +f|23*15|2|2013||Winter vacations (B)|multi +f|22*15|2|2014||Winter vacations (B)|multi diff --git a/holidays/french_holidays.FR.dat b/holidays/french_holidays.FR.dat index 182cb7e..e4316f3 100644 --- a/holidays/french_holidays.FR.dat +++ b/holidays/french_holidays.FR.dat @@ -5,7 +5,8 @@ # ge: Georgios' name day, Orthodox Easter dependent holiday, annually # ce: Catholic Easter holiday -# type|day|month|year|footer|header|off +# type|day*span|month|year|footer|header|off +# day*span supported only for f a|1|1|||Nouvel an|off a|1|5|||Fête du Travail|off @@ -21,3 +22,6 @@ ce|0||||Pâques|off ce|1||||Lundi de Pâques|off ce|39||||Ascension|off ce|50||||Lundi de Pentecôte|off + +f|23*15|2|2013||Vacances d'hiver (B)|multi +f|22*15|2|2014||Vacances d'hiver (B)|multi diff --git a/holidays/greek_holidays.EL.dat b/holidays/greek_holidays.EL.dat index 7557766..739231c 100644 --- a/holidays/greek_holidays.EL.dat +++ b/holidays/greek_holidays.EL.dat @@ -5,7 +5,8 @@ # ge: Georgios' name day, Orthodox Easter dependent holiday, annually # ce: Catholic Easter holiday -# type|day|month|year|footer|header|off +# type|day*span|month|year|footer|header|off +# day*span supported only for f a|1|1|||ΠΡΩΤΟΧΡΟΝΙΑ|off a|6|1|||ΘΕΟΦΑΝΕΙΑ|off diff --git a/holidays/greek_namedays.EL.dat b/holidays/greek_namedays.EL.dat index 9168185..5917ef8 100644 --- a/holidays/greek_namedays.EL.dat +++ b/holidays/greek_namedays.EL.dat @@ -1,3 +1,12 @@ +# a: holiday occurs annually fixed day/month +# m: holiday occurs monthly, fixed day +# f: fixed day/month/year combination (e.g. deadline, trip, etc.) +# oe: Orthodox Easter-dependent holiday, annually +# ge: Georgios' name day, Orthodox Easter dependent holiday, annually +# ce: Catholic Easter holiday + +# type|day*span|month|year|footer|header|off +# day*span supported only for f a|1|1||Βασίλης|| a|6|1||Φώτης, Φάνης, Ιορδάνης|| a|7|1||Ιωάννης Πρόδρομος|| diff --git a/lib/holiday.py b/lib/holiday.py index 6f4e1c9..6d8ab29 100644 --- a/lib/holiday.py +++ b/lib/holiday.py @@ -55,10 +55,19 @@ class Holiday(object): Properties: header: string for header footer: string for footer - flags : bit combination of {OFF=1} + flags : bit combination of {OFF=1, MULTI=2} OFF: day off (real holiday) + MULTI: multi-day event (used to mark long day ranges, + not necessarily holidays) + + Remarks: + Rendering style is considered in the following order: + 1) OFF + 2) MULTI + First flag that matches determines the style """ OFF = 1 + MULTI = 2 def __init__(self, header = [], footer = [], flags_str = None): self.header_list = self._strip_empty(header) self.footer_list = self._strip_empty(footer) @@ -85,6 +94,7 @@ class Holiday(object): val = 0 for s in fs: if s == 'off': val |= Holiday.OFF + elif s == 'multi': val |= Holiday.MULTI return val def _strip_empty(self, sl): @@ -98,7 +108,7 @@ class Holiday(object): return res class HolidayProvider(object): - def __init__(self, s_normal, s_weekend, s_holiday, s_weekend_holiday): + def __init__(self, s_normal, s_weekend, s_holiday, s_weekend_holiday, s_multi, s_weekend_multi): self.annual = dict() # key = (d,m) self.monthly = dict() # key = d self.fixed = dict() # key = date() @@ -111,13 +121,22 @@ class HolidayProvider(object): self.s_weekend = s_weekend self.s_holiday = s_holiday self.s_weekend_holiday = s_weekend_holiday + self.s_multi = s_multi + self.s_weekend_multi = s_weekend_multi def parse_day_record(self, fields): + """return tuple (etype,d,m,y,footer,header,flags)""" if len(fields) != 7: raise ValueError("Too many fields: " + str(fields)) for i in range(len(fields)): if len(fields[i]) == 0: fields[i] = None - d = int(fields[1]) if fields[1] else 0 + if fields[1]: + if '*' in fields[1]: + d = map(int,fields[1].split('*')) + if fields[0] != 'f': + raise ValueError("multi-day events not allowed with event type '%s'" % fields[0]) + else: d = int(fields[1]) + else: d = 0 m = int(fields[2]) if fields[2] else 0 y = int(fields[3]) if fields[3] else 0 return (fields[0],d,m,y,fields[4],fields[5],fields[6]) @@ -131,9 +150,9 @@ class HolidayProvider(object): def load_holiday_file(self, filename): with open(filename, 'r') as f: for line in f: - if line[0] == '#': continue - line = line.rstrip() + line = line.strip() if not line: continue + if line[0] == '#': continue fields = line.split('|') etype,d,m,y,footer,header,flags = self.parse_day_record(fields) hol = Holiday([header], [footer], flags) @@ -144,8 +163,20 @@ class HolidayProvider(object): if d not in self.monthly: self.monthly[d] = [] self.monthly[d].append(hol) elif etype == 'f': - if date(y,m,d) not in self.fixed: self.fixed[date(y,m,d)] = [] - self.fixed[date(y,m,d)].append(hol) + day = d if type(d) is int else d[0] + if date(y,m,day) not in self.fixed: self.fixed[date(y,m,day)] = [] + self.fixed[date(y,m,day)].append(hol) + # use header/footer only on first day of multi-day events, + # or on the first day of month (proposed by Neels) + if type(d) is list: + dt = date(y,m,d[0]) + timedelta(1) + dt2 = dt + timedelta(d[1]) + hol2 = Holiday([], [], flags) + while dt != dt2: + if dt not in self.fixed: self.fixed[dt] = [] + self.fixed[dt].append(hol2 if dt.day > 1 else hol) + dt += timedelta(1) + elif etype == 'oe': if d not in self.orth_easter: self.orth_easter[d] = [] self.orth_easter[d].append(hol) @@ -197,6 +228,13 @@ class HolidayProvider(object): dt = date(y,m,d) return self.cache[dt] if dt in self.cache else None + def get_style(self, flags, dow): + if flags & Holiday.OFF: + return self.s_weekend_holiday if dow >= 5 else self.s_holiday + if flags & Holiday.MULTI: + return self.s_weekend_multi if dow >= 5 else self.s_multi + return self.s_weekend if dow >= 5 else self.s_normal + def __call__(self, year, month, dom, dow): """returns (header,footer,day_style) @@ -207,17 +245,13 @@ class HolidayProvider(object): """ hol = self.get_holiday(year,month,dom) if hol: - if hol.flags & Holiday.OFF: - style = self.s_weekend_holiday if dow >= 5 else self.s_holiday - else: - style = self.s_weekend if dow >= 5 else self.s_normal - return (hol.header(),hol.footer(),style) + return (hol.header(),hol.footer(),self.get_style(hol.flags,dow)) else: - return (None,None,self.s_weekend if dow >= 5 else self.s_normal) + return (None,None,self.get_style(0,dow)) if __name__ == '__main__': import sys - hp = HolidayProvider('n', 'w', 'h', 'wh') + hp = HolidayProvider('n', 'w', 'h', 'wh', 'm', 'wm') y = int(sys.argv[1]) for f in sys.argv[2:]: hp.load_holiday_file(f) diff --git a/style/bw.py b/style/bw.py index bba007e..ced152c 100644 --- a/style/bw.py +++ b/style/bw.py @@ -41,11 +41,19 @@ class dom_weekend(dom): class dom_holiday(dom): fg = (0,0,0) + bg = (0.95,0.95,0.95) header = (0,0,0) font = ("Times New Roman", 0, 1) class dom_weekend_holiday(dom_holiday): - bg = (0,0,0) + fg = (0,0,0) + bg = (0.95,0.95,0.95) + +class dom_multi(dom_holiday): + pass + +class dom_weekend_multi(dom_weekend_holiday) + pass class month: font = ("Times New Roman", 0, 1) diff --git a/style/default.py b/style/default.py index b316d56..56510b8 100644 --- a/style/default.py +++ b/style/default.py @@ -38,6 +38,7 @@ class dom_weekend(dom): bg = (0.7,1,1) fg = (0,0,1) +# OFF flag class dom_holiday(dom): bg = (0.7,1,1) fg = (1,0,0) @@ -45,7 +46,12 @@ class dom_holiday(dom): class dom_weekend_holiday(dom_holiday): pass - #bg = (0.7,1,1) + +class dom_multi(dom): + bg = (0.7,1,1) + +class dom_weekend_multi(dom_multi): + pass class month: font = ("Times New Roman", 0, 1) diff --git a/style/gfs.py b/style/gfs.py index e0362a3..0958e6c 100644 --- a/style/gfs.py +++ b/style/gfs.py @@ -16,10 +16,9 @@ # --- style.gfs (Greek Font Society fonts) --- -# day of week - import default +# day of week class dow(default.dow): font = "GFS Neohellenic" @@ -40,5 +39,13 @@ class dom_weekend_holiday(default.dom_weekend_holiday): font = "GFS Bodoni" header_font = footer_font = "GFS Elpis" +class dom_multi(default.dom_multi): + font = ("GFS Bodoni",) + header_font = footer_font = "GFS Elpis" + +class dom_weekend_multi(default.dom_weekend_multi): + font = "GFS Bodoni" + header_font = footer_font = "GFS Elpis" + class month(default.month): font = ("GFS Artemisia", 0, 1) diff --git a/style/rainbow-gfs.py b/style/rainbow-gfs.py index b0fa2ad..2e3d0f8 100644 --- a/style/rainbow-gfs.py +++ b/style/rainbow-gfs.py @@ -16,10 +16,10 @@ # --- style.rainbow --- -# day of week import gfs +# day of week class dow(gfs.dow): pass # day of month @@ -31,6 +31,10 @@ class dom_holiday(gfs.dom_holiday): pass class dom_weekend_holiday(gfs.dom_weekend_holiday): pass +class dom_multi(gfs.dom_multi): pass + +class dom_weekend_multi(gfs.dom_weekend_multi): pass + from lib.geom import color_mix, color_scale, color_auto_fg class month(gfs.month): diff --git a/style/rainbow.py b/style/rainbow.py index cb51165..aad7eba 100644 --- a/style/rainbow.py +++ b/style/rainbow.py @@ -16,10 +16,10 @@ # --- style.rainbow --- -# day of week import default +# day of week class dow(default.dow): pass # day of month @@ -31,6 +31,10 @@ class dom_holiday(default.dom_holiday): pass class dom_weekend_holiday(default.dom_weekend_holiday): pass +class dom_multi(default.dom_multi): pass + +class dom_weekend_multi(default.dom_weekend_multi): pass + from lib.geom import color_mix, color_scale, color_auto_fg class month(default.month): From 7ac9b54eaa617ec140eea1d1a0976bbb5c8b8c93 Mon Sep 17 00:00:00 2001 From: "geortz@gmail.com" Date: Mon, 30 Dec 2013 21:29:43 +0000 Subject: [PATCH 04/17] improved multi-holiday support git-svn-id: https://callirhoe.googlecode.com/svn/branches/next@23 81c8bb96-aa45-f2e2-0eef-c4fa4a15c6df --- callirhoe.py | 2 +- holidays/french_holidays.EN.dat | 5 +-- holidays/french_holidays.FR.dat | 5 +-- holidays/greek_holidays.EL.dat | 1 + holidays/greek_namedays.EL.dat | 2 ++ lib/holiday.py | 56 +++++++++++++++++++++++---------- style/bw.py | 2 +- 7 files changed, 50 insertions(+), 23 deletions(-) diff --git a/callirhoe.py b/callirhoe.py index 3205053..cf1fcf2 100755 --- a/callirhoe.py +++ b/callirhoe.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # callirhoe - high quality calendar rendering -# Copyright (C) 2012 George M. Tzoumas +# Copyright (C) 2012-2013 George M. Tzoumas # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/holidays/french_holidays.EN.dat b/holidays/french_holidays.EN.dat index 6ce81fe..f6bcf50 100644 --- a/holidays/french_holidays.EN.dat +++ b/holidays/french_holidays.EN.dat @@ -6,6 +6,7 @@ # ce: Catholic Easter holiday # type|day*span|month|year|footer|header|off +# type|day|month|year|footer|header|off # day*span supported only for f a|1|1|||New year's|off @@ -23,5 +24,5 @@ ce|1||||Easter Monday|off ce|39||||Ascension|off ce|50||||Whit Monday|off -f|23*15|2|2013||Winter vacations (B)|multi -f|22*15|2|2014||Winter vacations (B)|multi +f|23*15|2|2013|winter vacations (B)||multi +f|22*15|2|2014|winter vacations (B)||multi diff --git a/holidays/french_holidays.FR.dat b/holidays/french_holidays.FR.dat index e4316f3..dc3d740 100644 --- a/holidays/french_holidays.FR.dat +++ b/holidays/french_holidays.FR.dat @@ -6,6 +6,7 @@ # ce: Catholic Easter holiday # type|day*span|month|year|footer|header|off +# type|day|month|year|footer|header|off # day*span supported only for f a|1|1|||Nouvel an|off @@ -23,5 +24,5 @@ ce|1||||Lundi de Pâques|off ce|39||||Ascension|off ce|50||||Lundi de Pentecôte|off -f|23*15|2|2013||Vacances d'hiver (B)|multi -f|22*15|2|2014||Vacances d'hiver (B)|multi +f|23*16|2|2013|vacances d'hiver (B)||multi +f|22*16|2|2014|vacances d'hiver (B)||multi diff --git a/holidays/greek_holidays.EL.dat b/holidays/greek_holidays.EL.dat index 739231c..55ae129 100644 --- a/holidays/greek_holidays.EL.dat +++ b/holidays/greek_holidays.EL.dat @@ -6,6 +6,7 @@ # ce: Catholic Easter holiday # type|day*span|month|year|footer|header|off +# type|day|month|year|footer|header|off # day*span supported only for f a|1|1|||ΠΡΩΤΟΧΡΟΝΙΑ|off diff --git a/holidays/greek_namedays.EL.dat b/holidays/greek_namedays.EL.dat index 5917ef8..8caa10b 100644 --- a/holidays/greek_namedays.EL.dat +++ b/holidays/greek_namedays.EL.dat @@ -6,7 +6,9 @@ # ce: Catholic Easter holiday # type|day*span|month|year|footer|header|off +# type|day|month|year|footer|header|off # day*span supported only for f + a|1|1||Βασίλης|| a|6|1||Φώτης, Φάνης, Ιορδάνης|| a|7|1||Ιωάννης Πρόδρομος|| diff --git a/lib/holiday.py b/lib/holiday.py index 6d8ab29..3c1fa3d 100644 --- a/lib/holiday.py +++ b/lib/holiday.py @@ -125,22 +125,40 @@ class HolidayProvider(object): self.s_weekend_multi = s_weekend_multi def parse_day_record(self, fields): - """return tuple (etype,d,m,y,footer,header,flags)""" + """return tuple (etype,d,m,y,span,footer,header,flags)""" if len(fields) != 7: raise ValueError("Too many fields: " + str(fields)) for i in range(len(fields)): if len(fields[i]) == 0: fields[i] = None if fields[1]: if '*' in fields[1]: - d = map(int,fields[1].split('*')) if fields[0] != 'f': raise ValueError("multi-day events not allowed with event type '%s'" % fields[0]) - else: d = int(fields[1]) - else: d = 0 + d,span = map(int,fields[1].split('*')) + else: d,span = int(fields[1]), 1 + else: d,span = 0,0 m = int(fields[2]) if fields[2] else 0 y = int(fields[3]) if fields[3] else 0 - return (fields[0],d,m,y,fields[4],fields[5],fields[6]) + return (fields[0],d,m,y,span,fields[4],fields[5],fields[6]) + def multi_holiday_tuple(self, date1, date2, header, footer, flags): + """returns Holiday objects for (beginning, end, first_dom, rest)""" + if header: + header_tuple = (header+'..', '..'+header, '..'+header+'..', None) + else: + header_tuple = (None, None, None, None) + if footer: + footer_tuple = (footer+'..', '..'+footer, '..'+footer+'..', None) + else: + footer_tuple = (None, None, None, None) + return tuple(map(lambda k: Holiday([header_tuple[k]], [footer_tuple[k]], flags), + range(4))) + + # File Format: + # type|day*span|month|year|footer|header|off + # type|day|month|year|footer|header|off + # day*span supported only for f + # Type: # a: holiday occurs annually fixed day/month # m: holiday occurs monthly, fixed day # f: fixed day/month/year combination (e.g. deadline, trip, etc.) @@ -154,7 +172,7 @@ class HolidayProvider(object): if not line: continue if line[0] == '#': continue fields = line.split('|') - etype,d,m,y,footer,header,flags = self.parse_day_record(fields) + etype,d,m,y,span,footer,header,flags = self.parse_day_record(fields) hol = Holiday([header], [footer], flags) if etype == 'a': if (d,m) not in self.annual: self.annual[(d,m)] = [] @@ -163,18 +181,22 @@ class HolidayProvider(object): if d not in self.monthly: self.monthly[d] = [] self.monthly[d].append(hol) elif etype == 'f': - day = d if type(d) is int else d[0] - if date(y,m,day) not in self.fixed: self.fixed[date(y,m,day)] = [] - self.fixed[date(y,m,day)].append(hol) - # use header/footer only on first day of multi-day events, - # or on the first day of month (proposed by Neels) - if type(d) is list: - dt = date(y,m,d[0]) + timedelta(1) - dt2 = dt + timedelta(d[1]) - hol2 = Holiday([], [], flags) - while dt != dt2: + if span == 1: + if date(y,m,d) not in self.fixed: self.fixed[date(y,m,d)] = [] + self.fixed[date(y,m,d)].append(hol) + else: + # properly annotate multi-day events + dt1 = date(y,m,d) + dt2 = dt1 + timedelta(span-1) + hols = self.multi_holiday_tuple(dt1, dt2, header, footer, flags) + dt = dt1 + while dt <= dt2: if dt not in self.fixed: self.fixed[dt] = [] - self.fixed[dt].append(hol2 if dt.day > 1 else hol) + if dt == dt1: hol = hols[0] + elif dt == dt2: hol = hols[1] + elif dt.day == 1: hol = hols[2] + else: hol = hols[3] + self.fixed[dt].append(hol) dt += timedelta(1) elif etype == 'oe': diff --git a/style/bw.py b/style/bw.py index ced152c..3a0b702 100644 --- a/style/bw.py +++ b/style/bw.py @@ -52,7 +52,7 @@ class dom_weekend_holiday(dom_holiday): class dom_multi(dom_holiday): pass -class dom_weekend_multi(dom_weekend_holiday) +class dom_weekend_multi(dom_weekend_holiday): pass class month: From 8600fefd2132efd85295b00975779693cf1c805d Mon Sep 17 00:00:00 2001 From: "geortz@gmail.com" Date: Mon, 30 Dec 2013 22:41:14 +0000 Subject: [PATCH 05/17] dayname stuff DE language file {Neels} git-svn-id: https://callirhoe.googlecode.com/svn/branches/next@24 81c8bb96-aa45-f2e2-0eef-c4fa4a15c6df --- callirhoe.py | 11 ++++++----- holidays/french_holidays.EN.dat | 4 ++-- lang/DE.py | 34 +++++++++++++++++++++++++++++++++ lang/EL.py | 2 +- lang/EN.py | 2 +- lang/FR.py | 2 +- layouts/classic.py | 2 +- 7 files changed, 46 insertions(+), 11 deletions(-) create mode 100644 lang/DE.py diff --git a/callirhoe.py b/callirhoe.py index cf1fcf2..75dfeab 100755 --- a/callirhoe.py +++ b/callirhoe.py @@ -19,8 +19,8 @@ # TODO: -# fix overlapping multi-day events: -# use 'EVENT..', '..EVENT..', '..EVENT' formatting or sth. similar +# fix day names (length) +# fix odd-even year color (always start from dark) # allow to change background color (fill), other than white # page spec parse errors @@ -207,6 +207,7 @@ if options.geom_assign: calendar.month_name = Language.month_name calendar.day_name = Language.day_name +calendar.short_day_name = Language.short_day_name def itoa(s): try: @@ -262,8 +263,8 @@ hp = holiday.HolidayProvider(Style.dom, Style.dom_weekend, Style.dom_holiday, Style.dom_weekend_holiday, Style.dom_multi, Style.dom_weekend_multi) -print "Holidays: ", options.holidays -for f in options.holidays: - hp.load_holiday_file(f) +if options.holidays: + for f in options.holidays: + hp.load_holiday_file(f) Layout.draw_calendar(Outfile, Year, Month, MonthSpan, (Style,Geometry), hp, _version) diff --git a/holidays/french_holidays.EN.dat b/holidays/french_holidays.EN.dat index f6bcf50..02982f5 100644 --- a/holidays/french_holidays.EN.dat +++ b/holidays/french_holidays.EN.dat @@ -24,5 +24,5 @@ ce|1||||Easter Monday|off ce|39||||Ascension|off ce|50||||Whit Monday|off -f|23*15|2|2013|winter vacations (B)||multi -f|22*15|2|2014|winter vacations (B)||multi +f|23*16|2|2013|winter vacations (B)||multi +f|22*16|2|2014|winter vacations (B)||multi diff --git a/lang/DE.py b/lang/DE.py new file mode 100644 index 0000000..b043667 --- /dev/null +++ b/lang/DE.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- + +# callirhoe - high quality calendar rendering +# Copyright (C) 2012 George M. Tzoumas + +# German language data +# Copyright (C) 2013 Neels Hofmeyr + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see http://www.gnu.org/licenses/ + +day_name = [ u'Montag', u'Dienstag', u'Mittwoch', + u'Donnerstag', u'Freitag', u'Samstag', u'Sonntag' ] + +short_day_name = [ u'Mo', u'Di', u'Mi', u'Do', u'Fr', u'Sa', u'So' ] + +month_name = [ '', + u'Januar', u'Februar', u'März', u'April', + u'Mai', u'Juni', u'Juli', u'August', + u'September', u'Oktober', u'November', u'Dezember' ] + +short_month_name = [ '', + u'Jan', u'Feb', u'Mrz', u'Apr', u'Mai', u'Jun', + u'Jul', u'Aug', u'Sep', u'Okt', u'Nov', u'Dez' ] diff --git a/lang/EL.py b/lang/EL.py index 1a87f03..73bc92e 100644 --- a/lang/EL.py +++ b/lang/EL.py @@ -19,7 +19,7 @@ day_name = [ u'Δευτέρα', u'Τρίτη', u'Τετάρτη', u'Πέμπτη', u'Παρασκευή', u'Σάββατο', u'Κυριακή' ] -short_day_name = [ u'Δευ', u'Τρι', u'Τετ', u'Πεμ', u'Παρ', u'Σαβ', u'Κυρ' ] +short_day_name = [ u'Δε', u'Τρ', u'Τε', u'Πε', u'Πα', u'Σα', u'Κυ' ] month_name = [ '', u'Ιανουάριος', u'Φεβρουάριος', u'Μάρτιος', u'Απρίλιος', u'Μάιος', u'Ιούνιος', u'Ιούλιος', u'Αύγουστος', diff --git a/lang/EN.py b/lang/EN.py index 5dbb642..5bb58e4 100644 --- a/lang/EN.py +++ b/lang/EN.py @@ -19,7 +19,7 @@ day_name = [ u'Monday', u'Tuesday', u'Wednesday', u'Thursday', u'Friday', u'Saturday', u'Sunday' ] -short_day_name = [ u'Mon', u'Tue', u'Wed', u'Thu', u'Fri', u'Sat', u'Sun' ] +short_day_name = [ u'Mo', u'Tu', u'We', u'Th', u'Fr', u'Sa', u'Su' ] month_name = [ '', u'January', u'February', u'March', u'April', diff --git a/lang/FR.py b/lang/FR.py index 854297d..d0b00d6 100644 --- a/lang/FR.py +++ b/lang/FR.py @@ -19,7 +19,7 @@ day_name = [ u'Lundi', u'Mardi', u'Mercredi', u'Jeudi', u'Vendredi', u'Samedi', u'Dimanche' ] -short_day_name = [ u'Lun', u'Mar', u'Mer', u'Jeu', u'Ven', u'Sam', u'Dim' ] +short_day_name = [ u'Lu', u'Ma', u'Me', u'Je', u'Ve', u'Sa', u'Di' ] month_name = [ '', u'Janvier', u'Février', u'Mars', u'Avril', diff --git a/layouts/classic.py b/layouts/classic.py index 30ff102..ecd9fe2 100644 --- a/layouts/classic.py +++ b/layouts/classic.py @@ -123,7 +123,7 @@ def _draw_day_cell_long(cr, rect, day, header, footer, theme, show_day_name): align = (2,valign), font = S.font, measure = "88") # draw name of day if show_day_name: - draw_str(cr, text = calendar.day_name[day_of_week][0], rect = Rdow, stretch = -1, stroke_rgba = S.fg, + draw_str(cr, text = calendar.short_day_name[day_of_week], rect = Rdow, stretch = -1, stroke_rgba = S.fg, align = (2,valign), font = S.font, measure = "M") Rh, Rf = rect_vsplit(Rhf, *G.hf_vsplit) # draw header From f9140cb010ccfa3dba52cb297b7651f82a7632af Mon Sep 17 00:00:00 2001 From: "geortz@gmail.com" Date: Tue, 4 Feb 2014 00:10:19 +0000 Subject: [PATCH 06/17] short/long daynames/monthnames git-svn-id: https://callirhoe.googlecode.com/svn/branches/next@38 81c8bb96-aa45-f2e2-0eef-c4fa4a15c6df --- callirhoe.py | 5 +++-- holidays/greek_holidays.EL.dat | 2 +- lang/DE.py | 4 ++-- lang/EL.py | 4 ++-- lang/EN.py | 5 +++-- lang/FR.py | 4 ++-- lang/TR.py | 6 +++--- layouts/classic.py | 35 ++++++++++++++++++++++++++-------- 8 files changed, 43 insertions(+), 22 deletions(-) diff --git a/callirhoe.py b/callirhoe.py index 75dfeab..4232b04 100755 --- a/callirhoe.py +++ b/callirhoe.py @@ -205,8 +205,9 @@ if options.style_assign: if options.geom_assign: for x in options.geom_assign: exec "Geometry.%s" % x -calendar.month_name = Language.month_name -calendar.day_name = Language.day_name +calendar.long_month_name = Language.long_month_name +calendar.long_day_name = Language.long_day_name +calendar.short_month_name = Language.short_month_name calendar.short_day_name = Language.short_day_name def itoa(s): diff --git a/holidays/greek_holidays.EL.dat b/holidays/greek_holidays.EL.dat index 55ae129..868be53 100644 --- a/holidays/greek_holidays.EL.dat +++ b/holidays/greek_holidays.EL.dat @@ -30,4 +30,4 @@ oe|0||||ΑΓΙΟ ΠΑΣΧΑ|off oe|1||||2η ΜΕΡΑ ΠΑΣΧΑ|off oe|49||||ΠΕΝΤΗΚΟΣΤΗ| oe|50||||ΑΓΙΟΥ ΠΝΕΥΜΑΤΟΣ|off -ce|0||||Καθολικό Πάσχα| +#ce|0|||Καθολικό Πάσχα|| diff --git a/lang/DE.py b/lang/DE.py index b043667..453f6a3 100644 --- a/lang/DE.py +++ b/lang/DE.py @@ -19,12 +19,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see http://www.gnu.org/licenses/ -day_name = [ u'Montag', u'Dienstag', u'Mittwoch', +long_day_name = [ u'Montag', u'Dienstag', u'Mittwoch', u'Donnerstag', u'Freitag', u'Samstag', u'Sonntag' ] short_day_name = [ u'Mo', u'Di', u'Mi', u'Do', u'Fr', u'Sa', u'So' ] -month_name = [ '', +long_month_name = [ '', u'Januar', u'Februar', u'März', u'April', u'Mai', u'Juni', u'Juli', u'August', u'September', u'Oktober', u'November', u'Dezember' ] diff --git a/lang/EL.py b/lang/EL.py index 73bc92e..66a4a51 100644 --- a/lang/EL.py +++ b/lang/EL.py @@ -16,12 +16,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see http://www.gnu.org/licenses/ -day_name = [ u'Δευτέρα', u'Τρίτη', u'Τετάρτη', +long_day_name = [ u'Δευτέρα', u'Τρίτη', u'Τετάρτη', u'Πέμπτη', u'Παρασκευή', u'Σάββατο', u'Κυριακή' ] short_day_name = [ u'Δε', u'Τρ', u'Τε', u'Πε', u'Πα', u'Σα', u'Κυ' ] -month_name = [ '', u'Ιανουάριος', u'Φεβρουάριος', u'Μάρτιος', u'Απρίλιος', +long_month_name = [ '', u'Ιανουάριος', u'Φεβρουάριος', u'Μάρτιος', u'Απρίλιος', u'Μάιος', u'Ιούνιος', u'Ιούλιος', u'Αύγουστος', u'Σεπτέμβριος', u'Οκτώβριος', u'Νοέμβριος', u'Δεκέμβριος' ] diff --git a/lang/EN.py b/lang/EN.py index 5bb58e4..c86ba4c 100644 --- a/lang/EN.py +++ b/lang/EN.py @@ -16,12 +16,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see http://www.gnu.org/licenses/ -day_name = [ u'Monday', u'Tuesday', u'Wednesday', +long_day_name = [ u'Monday', u'Tuesday', u'Wednesday', u'Thursday', u'Friday', u'Saturday', u'Sunday' ] short_day_name = [ u'Mo', u'Tu', u'We', u'Th', u'Fr', u'Sa', u'Su' ] -month_name = [ '', +long_month_name = [ '', u'January', u'February', u'March', u'April', u'May', u'June', u'July', u'August', u'September', u'October', u'November', u'December' ] @@ -29,3 +29,4 @@ month_name = [ '', short_month_name = [ '', u'Jan', u'Feb', u'Mar', u'Apr', u'May', u'Jun', u'Jul', u'Aug', u'Sep', u'Oct', u'Nov', u'Dec' ] + diff --git a/lang/FR.py b/lang/FR.py index d0b00d6..f11c65f 100644 --- a/lang/FR.py +++ b/lang/FR.py @@ -16,12 +16,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see http://www.gnu.org/licenses/ -day_name = [ u'Lundi', u'Mardi', u'Mercredi', +long_day_name = [ u'Lundi', u'Mardi', u'Mercredi', u'Jeudi', u'Vendredi', u'Samedi', u'Dimanche' ] short_day_name = [ u'Lu', u'Ma', u'Me', u'Je', u'Ve', u'Sa', u'Di' ] -month_name = [ '', +long_month_name = [ '', u'Janvier', u'Février', u'Mars', u'Avril', u'Mai', u'Juin', u'Juillet', u'Août', u'Septembre', u'Octobre', u'Novembre', u'Décembre' ] diff --git a/lang/TR.py b/lang/TR.py index cf82b4e..bbe646b 100644 --- a/lang/TR.py +++ b/lang/TR.py @@ -19,13 +19,13 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see http://www.gnu.org/licenses/ -day_name = [ u'Pazartesi', u'Salı', u' Çarşamba ', +long_day_name = [ u'Pazartesi', u'Salı', u' Çarşamba ', u'Perşembe', u'Cuma', u'Cumartesi', u'Pazar' ] short_day_name = [ u'Pt', u'Sa', u'Ça', u'Pe', u'Cu', u'Ct', u'Pa' ] -month_name = [ '', u'Ocak', 'Şubat', u'Mart', u'Nisan', +long_month_name = [ '', u'Ocak', 'Şubat', u'Mart', u'Nisan', u'Mayıs', u'Haziran', u'Temmuz', u'Ağustos', u'Eylül', u'Ekim', u'Kasım', u'Aralık' ] -short_month_name = month_name +short_month_name = long_month_name diff --git a/layouts/classic.py b/layouts/classic.py index ecd9fe2..37b488f 100644 --- a/layouts/classic.py +++ b/layouts/classic.py @@ -40,6 +40,10 @@ parser.add_option("--z-order", choices=["auto", "increasing", "decreasing"], def "selects increasing order if and only if sloppy boxes are enabled [%default]") parser.add_option("--month-with-year", action="store_true", default=False, help="displays year together with month name, e.g. January 1980; suppresses year from footer line") +parser.add_option("--short-monthnames", action="store_true", default=False, + help="user the short version of month names (defined in language file) [%default]") +parser.add_option("--long-daynames", action="store_true", default=False, + help="user the long version of day names (defined in language file) [%default]") parser.add_option("--long-daycells", action="store_const", const=0.0, dest="short_daycell_ratio", help="force use of only long daycells") parser.add_option("--short-daycells", action="store_const", const=1.0e6, dest="short_daycell_ratio", @@ -72,7 +76,7 @@ parser.add_option("--swap-colors", action="store_true", default=None, help="swap month colors for even/odd years") -def weekrows_of_month(year, month): +def _weekrows_of_month(year, month): day,span = calendar.monthrange(year, month) if day == 0 and span == 28: return 4 if day == 5 and span == 31: return 6 @@ -123,8 +127,8 @@ def _draw_day_cell_long(cr, rect, day, header, footer, theme, show_day_name): align = (2,valign), font = S.font, measure = "88") # draw name of day if show_day_name: - draw_str(cr, text = calendar.short_day_name[day_of_week], rect = Rdow, stretch = -1, stroke_rgba = S.fg, - align = (2,valign), font = S.font, measure = "M") + draw_str(cr, text = calendar.day_name[day_of_week], rect = Rdow, stretch = -1, stroke_rgba = S.fg, + align = (0,valign), font = S.font, measure = "M") Rh, Rf = rect_vsplit(Rhf, *G.hf_vsplit) # draw header if header: @@ -146,11 +150,13 @@ def draw_month_matrix(cr, rect, month, year, theme, holiday_provider, daycell_th apply_rect(cr, rect, G.month.sloppy_dx, G.month.sloppy_dy, G.month.sloppy_rot) day, span = calendar.monthrange(year, month) - weekrows = 6 if G.month.symmetric else weekrows_of_month(year, month) + weekrows = 6 if G.month.symmetric else _weekrows_of_month(year, month) dom = -day + 1; wmeasure = 'A'*max(map(len,calendar.day_name)) mmeasure = 'A'*max(map(len,calendar.month_name)) - + if options.month_with_year: + mmeasure += 'A'*(len(str(year))+1) + grid = GLayout(rect_from_origin(rect), 7, 7) # 61.8% - 38.2% split (golden) R_mb, R_db = rect_vsplit(grid.item_span(1, 7, 0, 0), 0.618) # month name bar, day name bar @@ -212,9 +218,10 @@ def draw_month_bar(cr, rect, month, year, theme, holiday_provider, daycell_thres apply_rect(cr, rect, G.month.sloppy_dx, G.month.sloppy_dy, G.month.sloppy_rot) day, span = calendar.monthrange(year, month) - wmeasure = 'A'*max(map(len,calendar.day_name)) mmeasure = 'A'*max(map(len,calendar.month_name)) - + if options.month_with_year: + mmeasure += 'A'*(len(str(year))+1) + rows = 31 if G.month.symmetric else span grid = VLayout(rect_from_origin(rect), 32) # title bar always symmetric dom_grid = VLayout(grid.item_span(31,1), rows) @@ -252,7 +259,9 @@ def draw_month_bar(cr, rect, month, year, theme, holiday_provider, daycell_thres if S.month.text_shadow: f = S.month.text_shadow_size mshad = (f,-f) if G.landscape else (f,f) - draw_str(cr, text = calendar.month_name[month], rect = R_text, stretch = -1, stroke_rgba = mcolor_fg, + title_str = calendar.month_name[month] + if options.month_with_year: title_str += ' ' + str(year) + draw_str(cr, text = title_str, rect = R_text, stretch = -1, stroke_rgba = mcolor_fg, align = (2,0), font = S.month.font, measure = mmeasure, shadow = mshad) cr.restore() @@ -290,9 +299,19 @@ def draw_calendar(Outfile, Year, Month, MonthSpan, Theme, holiday_provider, vers G.month.padding = options.padding if options.no_shadow == True: S.month.box_shadow = False + if Year % 2: options.swap_colors = not options.swap_colors if options.swap_colors: S.month.color_map_bg = (S.month.color_map_bg[1], S.month.color_map_bg[0]) S.month.color_map_fg = (S.month.color_map_fg[1], S.month.color_map_fg[0]) + + if options.long_daynames: + calendar.day_name = calendar.long_day_name + else: + calendar.day_name = calendar.short_day_name + if options.short_monthnames: + calendar.month_name = calendar.short_month_name + else: + calendar.month_name = calendar.long_month_name try: page = PageWriter(Outfile, G.landscape, G.pagespec, G.border, not options.opaque) From a0acd071982b94f75594ecae6d35700168abf474 Mon Sep 17 00:00:00 2001 From: "geortz@gmail.com" Date: Fri, 7 Feb 2014 14:05:51 +0000 Subject: [PATCH 07/17] comment fix git-svn-id: https://callirhoe.googlecode.com/svn/branches/next@39 81c8bb96-aa45-f2e2-0eef-c4fa4a15c6df --- callirhoe.py | 1 - 1 file changed, 1 deletion(-) diff --git a/callirhoe.py b/callirhoe.py index 4232b04..b3948f3 100755 --- a/callirhoe.py +++ b/callirhoe.py @@ -20,7 +20,6 @@ # TODO: # fix day names (length) -# fix odd-even year color (always start from dark) # allow to change background color (fill), other than white # page spec parse errors From b30955c06ae3fc3e1065bd42d51bd58b0de6b2f3 Mon Sep 17 00:00:00 2001 From: "geortz@gmail.com" Date: Fri, 7 Feb 2014 16:44:00 +0000 Subject: [PATCH 08/17] new holiday format, supports both span and ranges git-svn-id: https://callirhoe.googlecode.com/svn/branches/next@40 81c8bb96-aa45-f2e2-0eef-c4fa4a15c6df --- callirhoe.py | 2 +- holidays/french_holidays.EN.dat | 49 +++++++----- holidays/french_holidays.FR.dat | 49 +++++++----- holidays/greek_holidays.EL.dat | 62 +++++++------- holidays/greek_namedays.EL.dat | 126 +++++++++++++++-------------- layouts/classic.py | 6 +- lib/holiday.py | 138 ++++++++++++++++++++++---------- 7 files changed, 252 insertions(+), 180 deletions(-) diff --git a/callirhoe.py b/callirhoe.py index b3948f3..e4de797 100755 --- a/callirhoe.py +++ b/callirhoe.py @@ -19,7 +19,7 @@ # TODO: -# fix day names (length) +# fix auto-measure rendering (cairo) # allow to change background color (fill), other than white # page spec parse errors diff --git a/holidays/french_holidays.EN.dat b/holidays/french_holidays.EN.dat index 02982f5..68a85e4 100644 --- a/holidays/french_holidays.EN.dat +++ b/holidays/french_holidays.EN.dat @@ -1,28 +1,33 @@ -# a: holiday occurs annually fixed day/month -# m: holiday occurs monthly, fixed day -# f: fixed day/month/year combination (e.g. deadline, trip, etc.) +# type|DATE*span|footer|header|flags +# type|DATE1-DATE2|footer|header|flags +# type|DATE|footer|header|flags +# +# type: +# d: event occurs annually fixed day/month: MMDD +# d: event occurs monthly, fixed day: DD +# d: fixed day/month/year combination (e.g. deadline, trip, etc.): YYYYMMDD # oe: Orthodox Easter-dependent holiday, annually # ge: Georgios' name day, Orthodox Easter dependent holiday, annually # ce: Catholic Easter holiday +# +# DATE*span and DATE1-DATE2 supported only for YYYYMMDD +# flags = {off, multi} -# type|day*span|month|year|footer|header|off -# type|day|month|year|footer|header|off -# day*span supported only for f +d|0101||New year's|off +d|0501||Labour day|off +d|0508||Victory in Europe|off +d|0714||Bastille|off +d|0815||Assumption of Mary|off +d|1101||All Saints|off +d|1111||Armistice|off +d|1225||Christmas|off +d|1226||St. Stephen's|off -a|1|1|||New year's|off -a|1|5|||Labour day|off -a|8|5|||Victory in Europe|off -a|14|7|||Bastille|off -a|15|8|||Assumption of Mary|off -a|1|11|||All Saints|off -a|11|11|||Armistice|off -a|25|12|||Christmas|off -a|26|12|||St. Stephen's|off -ce|-2||||Good Friday| -ce|0||||Easter|off -ce|1||||Easter Monday|off -ce|39||||Ascension|off -ce|50||||Whit Monday|off +ce|-2||Good Friday| +ce|0||Easter|off +ce|1||Easter Monday|off +ce|39||Ascension|off +ce|50||Whit Monday|off -f|23*16|2|2013|winter vacations (B)||multi -f|22*16|2|2014|winter vacations (B)||multi +d|20130223-20130310|winter vacations (B)||multi +f|20140222-20140309|winter vacations (B)||multi diff --git a/holidays/french_holidays.FR.dat b/holidays/french_holidays.FR.dat index dc3d740..bcc5036 100644 --- a/holidays/french_holidays.FR.dat +++ b/holidays/french_holidays.FR.dat @@ -1,28 +1,33 @@ -# a: holiday occurs annually fixed day/month -# m: holiday occurs monthly, fixed day -# f: fixed day/month/year combination (e.g. deadline, trip, etc.) +# type|DATE*span|footer|header|flags +# type|DATE1-DATE2|footer|header|flags +# type|DATE|footer|header|flags +# +# type: +# d: event occurs annually fixed day/month: MMDD +# d: event occurs monthly, fixed day: DD +# d: fixed day/month/year combination (e.g. deadline, trip, etc.): YYYYMMDD # oe: Orthodox Easter-dependent holiday, annually # ge: Georgios' name day, Orthodox Easter dependent holiday, annually # ce: Catholic Easter holiday +# +# DATE*span and DATE1-DATE2 supported only for YYYYMMDD +# flags = {off, multi} -# type|day*span|month|year|footer|header|off -# type|day|month|year|footer|header|off -# day*span supported only for f +d|0101||Nouvel an|off +d|0501||Fête du Travail|off +d|0508||Fête de la Victoire|off +d|0714||Fête nationale|off +d|0815||Assomption|off +d|1101||Toussaint|off +d|1111||Armistice de 1918|off +d|1225||Noël|off +d|1226||Saint-Étienne|off -a|1|1|||Nouvel an|off -a|1|5|||Fête du Travail|off -a|8|5|||Fête de la Victoire|off -a|14|7|||Fête nationale|off -a|15|8|||Assomption|off -a|1|11|||Toussaint|off -a|11|11|||Armistice de 1918|off -a|25|12|||Noël|off -a|26|12|||Saint-Étienne|off -ce|-2||||Vendredi saint| -ce|0||||Pâques|off -ce|1||||Lundi de Pâques|off -ce|39||||Ascension|off -ce|50||||Lundi de Pentecôte|off +ce|-2||Vendredi saint| +ce|0||Pâques|off +ce|1||Lundi de Pâques|off +ce|39||Ascension|off +ce|50||Lundi de Pentecôte|off -f|23*16|2|2013|vacances d'hiver (B)||multi -f|22*16|2|2014|vacances d'hiver (B)||multi +d|20130223-20130310|vacances d'hiver (B)||multi +d|20140222-20140309|vacances d'hiver (B)||multi diff --git a/holidays/greek_holidays.EL.dat b/holidays/greek_holidays.EL.dat index 868be53..5c0ba17 100644 --- a/holidays/greek_holidays.EL.dat +++ b/holidays/greek_holidays.EL.dat @@ -1,33 +1,39 @@ -# a: holiday occurs annually fixed day/month -# m: holiday occurs monthly, fixed day -# f: fixed day/month/year combination (e.g. deadline, trip, etc.) +# type|DATE*span|footer|header|flags +# type|DATE1-DATE2|footer|header|flags +# type|DATE|footer|header|flags +# +# type: +# d: event occurs annually fixed day/month: MMDD +# d: event occurs monthly, fixed day: DD +# d: fixed day/month/year combination (e.g. deadline, trip, etc.): YYYYMMDD # oe: Orthodox Easter-dependent holiday, annually # ge: Georgios' name day, Orthodox Easter dependent holiday, annually # ce: Catholic Easter holiday +# +# DATE*span and DATE1-DATE2 supported only for YYYYMMDD +# flags = {off, multi} -# type|day*span|month|year|footer|header|off -# type|day|month|year|footer|header|off -# day*span supported only for f +d|0101||ΠΡΩΤΟΧΡΟΝΙΑ|off +d|0106||ΘΕΟΦΑΝΕΙΑ|off +d|0325||ΕΥΑΓΓΕΛΙΣΜΟΣ|off +d|0501||ΠΡΩΤΟΜΑΓΙΑ|off +d|0815||ΚΟΙΜΗΣΗ ΘΕΟΤΟΚΟΥ|off +d|1028||ΕΠΕΤΕΙΟΣ ΤΟΥ «ΟΧΙ»|off +d|1117||ΠΟΛΥΤΕΧΝΕΙΟ| +d|1225||ΧΡΙΣΤΟΥΓΕΝΝΑ|off +d|1226||2η ΜΕΡΑ ΧΡΙΣΤΟΥΓ.|off -a|1|1|||ΠΡΩΤΟΧΡΟΝΙΑ|off -a|6|1|||ΘΕΟΦΑΝΕΙΑ|off -a|25|3|||ΕΥΑΓΓΕΛΙΣΜΟΣ|off -a|1|5|||ΠΡΩΤΟΜΑΓΙΑ|off -a|15|8|||ΚΟΙΜΗΣΗ ΘΕΟΤΟΚΟΥ|off -a|28|10|||ΕΠΕΤΕΙΟΣ ΤΟΥ «ΟΧΙ»|off -a|17|11|||ΠΟΛΥΤΕΧΝΕΙΟ| -a|25|12|||ΧΡΙΣΤΟΥΓΕΝΝΑ|off -a|26|12|||2η ΜΕΡΑ ΧΡΙΣΤΟΥΓ.|off -oe|-70||||Αρχή Τριωδίου|off -oe|-59||||Τσικνοπέμπτη| -oe|-56||||Της Απόκρεω| -oe|-49||||Της Τυροφάγου|off -oe|-48||||ΚΑΘΑΡΑ ΔΕΥΤΕΡΑ|off -oe|-8||||Σαβ. Λαζάρου| -oe|-7||||Κυρ. Βαϊων| -oe|-2||||ΜΕΓ. ΠΑΡΑΣΚΕΥΗ|off -oe|0||||ΑΓΙΟ ΠΑΣΧΑ|off -oe|1||||2η ΜΕΡΑ ΠΑΣΧΑ|off -oe|49||||ΠΕΝΤΗΚΟΣΤΗ| -oe|50||||ΑΓΙΟΥ ΠΝΕΥΜΑΤΟΣ|off -#ce|0|||Καθολικό Πάσχα|| +oe|-70||Αρχή Τριωδίου|off +oe|-59||Τσικνοπέμπτη| +oe|-56||Της Απόκρεω| +oe|-49||Της Τυροφάγου|off +oe|-48||ΚΑΘΑΡΑ ΔΕΥΤΕΡΑ|off +oe|-8||Σαβ. Λαζάρου| +oe|-7||Κυρ. Βαϊων| +oe|-2||ΜΕΓ. ΠΑΡΑΣΚΕΥΗ|off +oe|0||ΑΓΙΟ ΠΑΣΧΑ|off +oe|1||2η ΜΕΡΑ ΠΑΣΧΑ|off +oe|49||ΠΕΝΤΗΚΟΣΤΗ| +oe|50||ΑΓΙΟΥ ΠΝΕΥΜΑΤΟΣ|off + +#ce|0|Καθολικό Πάσχα|| diff --git a/holidays/greek_namedays.EL.dat b/holidays/greek_namedays.EL.dat index 8caa10b..207489c 100644 --- a/holidays/greek_namedays.EL.dat +++ b/holidays/greek_namedays.EL.dat @@ -1,66 +1,70 @@ -# a: holiday occurs annually fixed day/month -# m: holiday occurs monthly, fixed day -# f: fixed day/month/year combination (e.g. deadline, trip, etc.) +# type|DATE*span|footer|header|flags +# type|DATE1-DATE2|footer|header|flags +# type|DATE|footer|header|flags +# +# type: +# d: event occurs annually fixed day/month: MMDD +# d: event occurs monthly, fixed day: DD +# d: fixed day/month/year combination (e.g. deadline, trip, etc.): YYYYMMDD # oe: Orthodox Easter-dependent holiday, annually # ge: Georgios' name day, Orthodox Easter dependent holiday, annually # ce: Catholic Easter holiday +# +# DATE*span and DATE1-DATE2 supported only for YYYYMMDD +# flags = {off, multi} -# type|day*span|month|year|footer|header|off -# type|day|month|year|footer|header|off -# day*span supported only for f +d|0101|Βασίλης|| +d|0106|Φώτης, Φάνης, Ιορδάνης|| +d|0107|Ιωάννης Πρόδρομος|| +d|0117|Αντώνης, Γιώργος|| +d|0118|Αθανάσιος, Κύριλλος|| +d|0120|Ευθύμιος|| +d|0125|Γρηγόρης|| +d|0201|Τρύφων, Αναστάσιος|| +d|0207|Λουκάς|| +d|0210|Χαράλαμπος, Χαρίκλεια, Ζήνων|| +d|0214|Βαλεντίνος|| +d|0325|Ευάγγελος|| +d|0507|Ειρήνη|| +d|0509|Χριστόφορος|| +d|0521|Κωνσταντίνος, Ελένη|| +d|0630|Απόστολος|| +d|0701|Κοσμάς, Διαμιανός, Ανάργυρος|| +d|0711|Όλγα, Ευφημία|| +d|0717|Μαρίνα|| +d|0720|Ηλίας|| +d|0726|Παρασκευή|| +d|0727|Παντελεήμων|| +d|0806|Σωτήρης|| +d|0815|Παναγιώτης, Μαρία, Δέσποινα|| +d|0830|Αλέξανδρος|| +d|0914|Σταύρος|| +d|0917|Σοφία, Αγάπη, Ελπίδα|| +d|0920|Ευστάθιος|| +d|1026|Δημήτριος|| +d|1101|Κοσμάς, Δαμιανός, Ανάργυρος|| +d|1108|Άγγελος, Μιχ, Γαβρ, Ταξιάρ.|| +d|1109|Νεκτάριος|| +d|1110|Ορέστης|| +d|1114|Φίλιππος|| +d|1121|Εισόδια Θεοτόκου (Μαρία)|| +d|1125|Αικατερίνη, Μερκούριος|| +d|1126|Στυλιανός|| +d|1130|Ανδρέας|| +d|1204|Βαρβάρα|| +d|1205|Σάββας, Διογένης|| +d|1206|Νικόλαος|| +d|1209|Άννα|| +d|1212|Σπυρίδων|| +d|1215|Ελευθέριος, Ελευθερία|| +d|1222|Αναστασία|| +d|1224|Ευγενία|| +d|1225|Χρήστος, Χριστίνα|| +d|1227|Στέφανος|| -a|1|1||Βασίλης|| -a|6|1||Φώτης, Φάνης, Ιορδάνης|| -a|7|1||Ιωάννης Πρόδρομος|| -a|17|1||Αντώνης, Γιώργος|| -a|18|1||Αθανάσιος, Κύριλλος|| -a|20|1||Ευθύμιος|| -a|25|1||Γρηγόρης|| -a|1|2||Τρύφων, Αναστάσιος|| -a|7|2||Λουκάς|| -a|10|2||Χαράλαμπος, Χαρίκλεια, Ζήνων|| -a|14|2||Βαλεντίνος|| -a|25|3||Ευάγγελος|| -ge||||Γιώργος|| -a|7|5||Ειρήνη|| -a|9|5||Χριστόφορος|| -a|21|5||Κωνσταντίνος, Ελένη|| -a|30|6||Απόστολος|| -a|1|7||Κοσμάς, Διαμιανός, Ανάργυρος|| -a|11|7||Όλγα, Ευφημία|| -a|17|7||Μαρίνα|| -a|20|7||Ηλίας|| -a|26|7||Παρασκευή|| -a|27|7||Παντελεήμων|| -a|6|8||Σωτήρης|| -a|15|8||Παναγιώτης, Μαρία, Δέσποινα|| -a|30|8||Αλέξανδρος|| -a|14|9||Σταύρος|| -a|17|9||Σοφία, Αγάπη, Ελπίδα|| -a|20|9||Ευστάθιος|| -a|26|10||Δημήτριος|| -a|1|11||Κοσμάς, Δαμιανός, Ανάργυρος|| -a|8|11||Άγγελος, Μιχ, Γαβρ, Ταξιάρ.|| -a|9|11||Νεκτάριος|| -a|10|11||Ορέστης|| -a|14|11||Φίλιππος|| -a|21|11||Εισόδια Θεοτόκου (Μαρία)|| -a|25|11||Αικατερίνη, Μερκούριος|| -a|26|11||Στυλιανός|| -a|30|11||Ανδρέας|| -a|4|12||Βαρβάρα|| -a|5|12||Σάββας, Διογένης|| -a|6|12||Νικόλαος|| -a|9|12||Άννα|| -a|12|12||Σπυρίδων|| -a|15|12||Ελευθέριος, Ελευθερία|| -a|22|12||Αναστασία|| -a|24|12||Ευγενία|| -a|25|12||Χρήστος, Χριστίνα|| -a|27|12||Στέφανος|| -e|-43||||Αγίων Θεοδώρων| -e|0|||Αναστάσιος, Πασχάλης, Λάμπρος|| -e|-8|||Λάζαρος|| -e|-7|||Βάιος|| -e|7|||Θωμάς|| -e|56||||Αγίων Πάντων| +oe|-43|Αγίων Θεοδώρων|| +oe|0|Αναστάσιος, Πασχάλης, Λάμπρος|| +oe|-8|Λάζαρος|| +oe|-7|Βάιος|| +oe|7|Θωμάς|| +oe|56|Αγίων Πάντων|| diff --git a/layouts/classic.py b/layouts/classic.py index 37b488f..bad5b58 100644 --- a/layouts/classic.py +++ b/layouts/classic.py @@ -104,11 +104,13 @@ def _draw_day_cell_short(cr, rect, day, header, footer, theme, show_day_name): # draw header if header: R = rect_rel_scale(rect, G.header_size[0], G.header_size[1], 0, -1.0 + G.header_align) - draw_str(cr, text = header, rect = R, stretch = -1, stroke_rgba = S.header, font = S.header_font) + draw_str(cr, text = header, rect = R, stretch = -1, stroke_rgba = S.header, + font = S.header_font) # , measure = "MgMgMgMgMgMg" # draw footer if footer: R = rect_rel_scale(rect, G.footer_size[0], G.footer_size[1], 0, 1.0 - G.footer_align) - draw_str(cr, text = footer, rect = R, stretch = -1, stroke_rgba = S.footer, font = S.footer_font) + draw_str(cr, text = footer, rect = R, stretch = -1, stroke_rgba = S.footer, + font = S.footer_font) def _draw_day_cell_long(cr, rect, day, header, footer, theme, show_day_name): S,G = theme diff --git a/lib/holiday.py b/lib/holiday.py index 3c1fa3d..d5125f5 100644 --- a/lib/holiday.py +++ b/lib/holiday.py @@ -107,6 +107,16 @@ class Holiday(object): res += ', ' + s return res + +def _decode_date_str(ddef): + if len(ddef) == 2: + return (0,0,int(ddef)) + if len(ddef) == 4: + return (0,int(ddef[:2]),int(ddef[-2:])) + if len(ddef) == 8: + return (int(ddef[:4]),int(ddef[4:6]),int(ddef[-2:])) + raise ValueError("invalid date definition '%s'" % ddef) + class HolidayProvider(object): def __init__(self, s_normal, s_weekend, s_holiday, s_weekend_holiday, s_multi, s_weekend_multi): self.annual = dict() # key = (d,m) @@ -125,21 +135,51 @@ class HolidayProvider(object): self.s_weekend_multi = s_weekend_multi def parse_day_record(self, fields): - """return tuple (etype,d,m,y,span,footer,header,flags)""" - if len(fields) != 7: + """return tuple (etype,ddef,footer,header,flags) + + Remarks: + ddef is either None + or int + or ((y,m,d),) + or ((y,m,d),(y,m,d)) + """ + if len(fields) != 5: raise ValueError("Too many fields: " + str(fields)) for i in range(len(fields)): if len(fields[i]) == 0: fields[i] = None - if fields[1]: - if '*' in fields[1]: - if fields[0] != 'f': - raise ValueError("multi-day events not allowed with event type '%s'" % fields[0]) - d,span = map(int,fields[1].split('*')) - else: d,span = int(fields[1]), 1 - else: d,span = 0,0 - m = int(fields[2]) if fields[2] else 0 - y = int(fields[3]) if fields[3] else 0 - return (fields[0],d,m,y,span,fields[4],fields[5],fields[6]) + if fields[0] == 'd': + if fields[1]: + if '*' in fields[1]: + if fields[0] != 'd': + raise ValueError("multi-day events not allowed with event type '%s'" % fields[0]) + dstr,spanstr = fields[1].split('*') + if len(dstr) != 8: + raise ValueError("multi-day events allowed only with full date, not '%s'" % dstr) + span = int(spanstr) + y,m,d = _decode_date_str(dstr) + dt1 = date(y,m,d) + dt2 = dt1 + timedelta(span-1) + res = ((y,m,d),(dt2.year,dt2.month,dt2.day)) + elif '-' in fields[1]: + if fields[0] != 'd': + raise ValueError("multi-day events not allowed with event type '%s'" % fields[0]) + dstr,dstr2 = fields[1].split('-') + if len(dstr) != 8: + raise ValueError("multi-day events allowed only with full date, not '%s'" % dstr) + y,m,d = _decode_date_str(dstr) + y2,m2,d2 = _decode_date_str(dstr2) + res = ((y,m,d),(y2,m2,d2)) + else: + y,m,d = _decode_date_str(fields[1]) + if len(fields[1]) == 8: + res = ((y,m,d),(y,m,d)) + else: + res = ((y,m,d),) + else: + res = None + else: + res = int(fields[1]) + return (fields[0],res,fields[2],fields[3],fields[4]) def multi_holiday_tuple(self, date1, date2, header, footer, flags): """returns Holiday objects for (beginning, end, first_dom, rest)""" @@ -155,16 +195,20 @@ class HolidayProvider(object): range(4))) # File Format: - # type|day*span|month|year|footer|header|off - # type|day|month|year|footer|header|off - # day*span supported only for f - # Type: - # a: holiday occurs annually fixed day/month - # m: holiday occurs monthly, fixed day - # f: fixed day/month/year combination (e.g. deadline, trip, etc.) + # type|DATE*span|footer|header|flags + # type|DATE1-DATE2|footer|header|flags + # type|DATE|footer|header|flags + # + # type: + # d: event occurs annually fixed day/month: MMDD + # d: event occurs monthly, fixed day: DD + # d: fixed day/month/year combination (e.g. deadline, trip, etc.): YYYYMMDD # oe: Orthodox Easter-dependent holiday, annually # ge: Georgios' name day, Orthodox Easter dependent holiday, annually # ce: Catholic Easter holiday + # + # DATE*span and DATE1-DATE2 supported only for YYYYMMDD + # flags = {off, multi} def load_holiday_file(self, filename): with open(filename, 'r') as f: for line in f: @@ -172,39 +216,45 @@ class HolidayProvider(object): if not line: continue if line[0] == '#': continue fields = line.split('|') - etype,d,m,y,span,footer,header,flags = self.parse_day_record(fields) + etype,ddef,footer,header,flags = self.parse_day_record(fields) hol = Holiday([header], [footer], flags) - if etype == 'a': - if (d,m) not in self.annual: self.annual[(d,m)] = [] - self.annual[(d,m)].append(hol) - elif etype == 'm': - if d not in self.monthly: self.monthly[d] = [] - self.monthly[d].append(hol) - elif etype == 'f': - if span == 1: - if date(y,m,d) not in self.fixed: self.fixed[date(y,m,d)] = [] - self.fixed[date(y,m,d)].append(hol) - else: - # properly annotate multi-day events - dt1 = date(y,m,d) - dt2 = dt1 + timedelta(span-1) - hols = self.multi_holiday_tuple(dt1, dt2, header, footer, flags) - dt = dt1 - while dt <= dt2: - if dt not in self.fixed: self.fixed[dt] = [] - if dt == dt1: hol = hols[0] - elif dt == dt2: hol = hols[1] - elif dt.day == 1: hol = hols[2] - else: hol = hols[3] - self.fixed[dt].append(hol) - dt += timedelta(1) + if etype == 'd': + if len(ddef) == 1: + y,m,d = ddef[0] + if m > 0: # annual event + if (d,m) not in self.annual: self.annual[(d,m)] = [] + self.annual[(d,m)].append(hol) + else: # monthly event + if d not in self.monthly: self.monthly[d] = [] + self.monthly[d].append(hol) + else: # fixed date event + dt1,dt2 = date(*ddef[0]),date(*ddef[1]) + span = (dt2-dt1).days + 1 + if span == 1: + if dt1 not in self.fixed: self.fixed[dt1] = [] + self.fixed[dt1].append(hol) + else: + # properly annotate multi-day events + hols = self.multi_holiday_tuple(dt1, dt2, header, footer, flags) + dt = dt1 + while dt <= dt2: + if dt not in self.fixed: self.fixed[dt] = [] + if dt == dt1: hol = hols[0] + elif dt == dt2: hol = hols[1] + elif dt.day == 1: hol = hols[2] + else: hol = hols[3] + self.fixed[dt].append(hol) + dt += timedelta(1) elif etype == 'oe': + d = ddef if d not in self.orth_easter: self.orth_easter[d] = [] self.orth_easter[d].append(hol) elif etype == 'ge': + d = ddef self.george.append(hol) elif etype == 'ce': + d = ddef if d not in self.cath_easter: self.cath_easter[d] = [] self.cath_easter[d].append(hol) From 8d3ee39b849513667a8055e6007f5b80465df236 Mon Sep 17 00:00:00 2001 From: "geortz@gmail.com" Date: Tue, 25 Feb 2014 18:20:09 +0000 Subject: [PATCH 09/17] separate bars layout [WIP] reminder day flag [WIP] minor fixes/additions in xcairo improved language data handling git-svn-id: https://callirhoe.googlecode.com/svn/branches/next@41 81c8bb96-aa45-f2e2-0eef-c4fa4a15c6df --- callirhoe.py | 18 ++- layouts/bars.py | 301 +++++++++++++++++++++++++++++++++++++++++++++ layouts/classic.py | 101 +++------------ lib/holiday.py | 19 ++- lib/xcairo.py | 15 +++ 5 files changed, 360 insertions(+), 94 deletions(-) create mode 100644 layouts/bars.py diff --git a/callirhoe.py b/callirhoe.py index e4de797..9350421 100755 --- a/callirhoe.py +++ b/callirhoe.py @@ -19,8 +19,10 @@ # TODO: +# separate bar/matrix [WIP] - move common code aside (base class?) # fix auto-measure rendering (cairo) + # allow to change background color (fill), other than white # page spec parse errors # mobile themes (e.g. 800x480) @@ -259,12 +261,22 @@ xcairo.XDPI = options.dpi Geometry.pagespec = options.paper Geometry.border = options.border -hp = holiday.HolidayProvider(Style.dom, Style.dom_weekend, +hprovider = holiday.HolidayProvider(Style.dom, Style.dom_weekend, Style.dom_holiday, Style.dom_weekend_holiday, Style.dom_multi, Style.dom_weekend_multi) if options.holidays: for f in options.holidays: - hp.load_holiday_file(f) + hprovider.load_holiday_file(f) -Layout.draw_calendar(Outfile, Year, Month, MonthSpan, (Style,Geometry), hp, _version) +if options.long_daynames: + Language.day_name = Language.long_day_name +else: + Language.day_name = Language.short_day_name + +if options.short_monthnames: + Language.month_name = Language.short_month_name +else: + Language.month_name = Language.long_month_name + +Layout.draw_calendar(Outfile, Year, Month, MonthSpan, (Style,Geometry,Language), hprovider, _version) diff --git a/layouts/bars.py b/layouts/bars.py new file mode 100644 index 0000000..98820d3 --- /dev/null +++ b/layouts/bars.py @@ -0,0 +1,301 @@ +# -*- coding: utf-8 -*- +# callirhoe - high quality calendar rendering +# Copyright (C) 2012-2013 George M. Tzoumas + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see http://www.gnu.org/licenses/ + +# --- layouts.bars --- + +from lib.xcairo import * +from lib.geom import * +from math import floor, ceil, sqrt +import calendar +import optparse +import sys +from datetime import date, timedelta + +parser = optparse.OptionParser(usage="%prog (...) --layout classic [options] (...)",add_help_option=False) +parser.add_option("--rows", type="int", default=0, help="force grid rows [%default]") +parser.add_option("--cols", type="int", default=0, + help="force grid columns [%default]; if ROWS and COLS are both non-zero, " + "calendar will span multiple pages as needed; if one value is zero, it " + "will be computed automatically in order to fill exactly 1 page") +parser.add_option("--grid-order", choices=["row","column"],default="row", + help="either `row' or `column' to set grid placing order row-wise or column-wise [%default]") +parser.add_option("--z-order", choices=["auto", "increasing", "decreasing"], default="auto", + help="either `increasing' or `decreasing' to set whether next month (in grid order) " + "lies above or below the previously drawn month; this affects shadow casting, " + "since rendering is always performed in increasing z-order; specifying `auto' " + "selects increasing order if and only if sloppy boxes are enabled [%default]") +parser.add_option("--month-with-year", action="store_true", default=False, + help="displays year together with month name, e.g. January 1980; suppresses year from footer line") +parser.add_option("--short-monthnames", action="store_true", default=False, + help="user the short version of month names (defined in language file) [%default]") +parser.add_option("--long-daynames", action="store_true", default=False, + help="user the long version of day names (defined in language file) [%default]") +parser.add_option("--long-daycells", action="store_const", const=0.0, dest="short_daycell_ratio", + help="force use of only long daycells") +parser.add_option("--short-daycells", action="store_const", const=1.0e6, dest="short_daycell_ratio", + help="force use of only short daycells") +parser.add_option("--short-daycell-ratio", type="float", default=2.5, + help="ratio threshold for day cells below which short version is drawn [%default]") +parser.add_option("--no-footer", action="store_true", default=False, + help="disable footer line (with year and rendered-by message)") +parser.add_option("--symmetric", action="store_true", default=False, + help="force symmetric mode (equivalent to --geom-var=month.symmetric=1). " + "In symmetric mode, day cells are equally sized and all month boxes contain " + "the same number of (possibly empty) cells, independently of how many days or " + "weeks per month. In asymmetric mode, empty rows are eliminated, by slightly " + "resizing day cells, in order to have uniform month boxes.") +parser.add_option("--padding", type="float", default=None, + help="set month box padding (equivalent to --geom-var=month.padding=PADDING); " + "month bars look better with smaller padding, while matrix mode looks better with " + "larger padding") +parser.add_option("--no-shadow", action="store_true", default=None, + help="disable box shadows") +parser.add_option("--opaque", action="store_true", default=False, + help="make background opaque (white fill)") +parser.add_option("--swap-colors", action="store_true", default=None, + help="swap month colors for even/odd years") + + +def _draw_day_cell_short(cr, rect, day, header, footer, theme, show_day_name): + S,G,L = theme + x, y, w, h = rect + day_of_month, day_of_week = day + draw_box(cr, rect, S.frame, S.bg, mm_to_dots(S.frame_thickness)) + R = rect_rel_scale(rect, G.size[0], G.size[1]) + if show_day_name: + Rdom, Rdow = rect_hsplit(R, *G.mw_split) + else: + Rdom = R + valign = 0 if show_day_name else 2 + # draw day of month (number) + draw_str(cr, text = str(day_of_month), rect = Rdom, stretch = -1, stroke_rgba = S.fg, + align = (2,valign), font = S.font, measure = "88") + # draw name of day + if show_day_name: + draw_str(cr, text = L.day_name[day_of_week][0], rect = Rdow, stretch = -1, stroke_rgba = S.fg, + align = (2,valign), font = S.font, measure = "88") + # draw header + if header: + R = rect_rel_scale(rect, G.header_size[0], G.header_size[1], 0, -1.0 + G.header_align) + draw_str(cr, text = header, rect = R, stretch = -1, stroke_rgba = S.header, + font = S.header_font) # , measure = "MgMgMgMgMgMg" + # draw footer + if footer: + R = rect_rel_scale(rect, G.footer_size[0], G.footer_size[1], 0, 1.0 - G.footer_align) + draw_str(cr, text = footer, rect = R, stretch = -1, stroke_rgba = S.footer, + font = S.footer_font) + +def _draw_day_cell_long(cr, rect, day, header, footer, theme, show_day_name): + S,G,L = theme + x, y, w, h = rect + day_of_month, day_of_week = day + draw_box(cr, rect, S.frame, S.bg, mm_to_dots(S.frame_thickness)) + R1, Rhf = rect_hsplit(rect, *G.hf_hsplit) + if show_day_name: + R = rect_rel_scale(R1, G.size[2], G.size[3]) + Rdom, Rdow = rect_hsplit(R, *G.mw_split) + else: + Rdom = rect_rel_scale(R1, G.size[0], G.size[1]) + valign = 0 if show_day_name else 2 + # draw day of month (number) + draw_str(cr, text = str(day_of_month), rect = Rdom, stretch = -1, stroke_rgba = S.fg, + align = (2,valign), font = S.font, measure = "88") + # draw name of day + if show_day_name: + draw_str(cr, text = L.day_name[day_of_week], rect = Rdow, stretch = -1, stroke_rgba = S.fg, + align = (0,valign), font = S.font, measure = "M") + Rh, Rf = rect_vsplit(Rhf, *G.hf_vsplit) + # draw header + if header: + draw_str(cr, text = header, rect = Rh, stretch = -1, stroke_rgba = S.header, align = (1,2), + font = S.header_font) + # draw footer + if footer: + draw_str(cr, text = footer, rect = Rf, stretch = -1, stroke_rgba = S.footer, align = (1,2), + font = S.footer_font) + +def draw_day_cell(cr, rect, day, header, footer, theme, show_day_name, short_thres): + if rect_ratio(rect) < short_thres: + _draw_day_cell_short(cr, rect, day, header, footer, theme, show_day_name) + else: + _draw_day_cell_long(cr, rect, day, header, footer, theme, show_day_name) + +def draw_month(cr, rect, month, year, theme, holiday_provider, daycell_thres = 2.5): + S,G,L = theme + apply_rect(cr, rect, G.month.sloppy_dx, G.month.sloppy_dy, G.month.sloppy_rot) + + day, span = calendar.monthrange(year, month) + mmeasure = 'A'*max(map(len,L.month_name)) + if options.month_with_year: + mmeasure += 'A'*(len(str(year))+1) + + rows = 31 if G.month.symmetric else span + grid = VLayout(rect_from_origin(rect), 32) # title bar always symmetric + dom_grid = VLayout(grid.item_span(31,1), rows) + + # draw box shadow + if S.month.box_shadow: + f = S.month.box_shadow_size + shad = (f,-f) if G.landscape else (f,f) + draw_shadow(cr, rect_from_origin(rect), shad) + + # draw day cells + for dom in range(1,rows+1): + R = dom_grid.item(dom-1) + if dom <= span: + holiday_tuple = holiday_provider(year, month, dom, day) + day_style = holiday_tuple[2] + draw_day_cell(cr, rect = R, day = (dom, day), header = holiday_tuple[0], footer = holiday_tuple[1], + theme = (day_style, G.dom, L), show_day_name = True, short_thres = daycell_thres) + else: + day_style = S.dom + draw_box(cr, rect = R, stroke_rgba = day_style.frame, fill_rgba = day_style.bg, + stroke_width = mm_to_dots(day_style.frame_thickness)) + day = (day + 1) % 7 + + # draw month title (name) + mcolor = S.month.color_map_bg[year%2][month] + mcolor_fg = S.month.color_map_fg[year%2][month] + R_mb = grid.item(0) + draw_box(cr, rect = R_mb, stroke_rgba = S.month.frame, fill_rgba = mcolor, + stroke_width = mm_to_dots(S.month.frame_thickness)) # title box + draw_box(cr, rect = rect_from_origin(rect), stroke_rgba = S.month.frame, fill_rgba = (), + stroke_width = mm_to_dots(S.month.frame_thickness)) # full box + R_text = rect_rel_scale(R_mb, 1, 0.5) + mshad = None + if S.month.text_shadow: + f = S.month.text_shadow_size + mshad = (f,-f) if G.landscape else (f,f) + title_str = L.month_name[month] + if options.month_with_year: title_str += ' ' + str(year) + draw_str(cr, text = title_str, rect = R_text, stretch = -1, stroke_rgba = mcolor_fg, + align = (2,0), font = S.month.font, measure = mmeasure, shadow = mshad) + cr.restore() + + +#1 1 1 +#2 2 1 +#3 3 1 + +#4 2 2 +#5 3 2 +#6 3 2 +#7 4 2 +#8 4 2 + +#9 3 3 +#10 4 3 +#11 4 3 +#12 4 3 + +#rows = 0 +#cols = 0 + +def draw_calendar(Outfile, Year, Month, MonthSpan, Theme, holiday_provider, version_string): + S,G,L = Theme + rows, cols = options.rows, options.cols + + if options.symmetric: + G.month.symmetric = True + if options.padding is not None: + G.month.padding = options.padding + if options.no_shadow == True: + S.month.box_shadow = False + if Year % 2: options.swap_colors = not options.swap_colors + if options.swap_colors: + S.month.color_map_bg = (S.month.color_map_bg[1], S.month.color_map_bg[0]) + S.month.color_map_fg = (S.month.color_map_fg[1], S.month.color_map_fg[0]) + + try: + page = PageWriter(Outfile, G.landscape, G.pagespec, G.border, not options.opaque) + except InvalidFormat as e: + print >> sys.stderr, "invalid output format", e.args[0] + sys.exit(1) + + if rows == 0 and cols == 0: +# if MonthSpan < 4: +# cols = 1; rows = MonthSpan +# elif MonthSpan < 9: +# cols = 2; rows = int(math.ceil(MonthSpan/2.0)) +# else: + # TODO: improve this heuristic + cols = int(floor(sqrt(MonthSpan))) + rows = cols + if rows*cols < MonthSpan: rows += 1 + if rows*cols < MonthSpan: rows += 1 + if rows*cols < MonthSpan: cols += 1; rows -= 1 + if G.landscape: rows, cols = cols, rows + elif rows == 0: + rows = int(ceil(MonthSpan*1.0/cols)) + elif cols == 0: + cols = int(ceil(MonthSpan*1.0/rows)) + G.landscape = page.landscape # PNG is pseudo-landscape (portrait with width>height) + + if not options.no_footer: + V0 = VLayout(page.Text_rect, 40, (1,)*4) + Rcal = V0.item_span(39,0) + Rc = rect_rel_scale(V0.item(39),1,0.5,0,0) + else: + Rcal = page.Text_rect + + grid = GLayout(Rcal, rows, cols, pad = (mm_to_dots(G.month.padding),)*4) + mpp = grid.count() # months per page + num_pages = int(ceil(MonthSpan*1.0/mpp)) + cur_month = Month + cur_year = Year + num_placed = 0 + page_layout = [] + for k in xrange(num_pages): + page_layout.append([]) + for i in xrange(mpp): + page_layout[k].append((cur_month,cur_year)) + num_placed += 1 + cur_month += 1 + if cur_month > 12: cur_month = 1; cur_year += 1 + if num_placed >= MonthSpan: break + + num_pages_written = 0 + + z_order = options.z_order + if z_order == "auto": + if G.month.sloppy_dx != 0 or G.month.sloppy_dy != 0 or G.month.sloppy_rot != 0: + z_order = "decreasing" + else: + z_order = "increasing" + for p in page_layout: + num_placed = 0 + yy = [p[0][1]] + if z_order == "decreasing": p.reverse() + for (m,y) in p: + k = len(p) - num_placed - 1 if z_order == "decreasing" else num_placed + draw_month(page.cr, grid.item_seq(k, options.grid_order == "column"), + month=m, year=y, theme = Theme, holiday_provider = holiday_provider, + daycell_thres = options.short_daycell_ratio) + num_placed += 1 + if (y > yy[-1]): yy.append(y) + if not options.month_with_year and not options.no_footer: + year_str = str(yy[0]) if yy[0] == yy[-1] else "%s – %s" % (yy[0],yy[-1]) + draw_str(page.cr, text = year_str, rect = Rc, stroke_rgba = (0,0,0,0.5), stretch = -1, + align = (0,0), font = (extract_font_name(S.month.font),0,0)) + if not options.no_footer: + draw_str(page.cr, text = "rendered by Callirhoe ver. %s" % version_string, + rect = Rc, stroke_rgba = (0,0,0,0.5), stretch = -1, align = (1,0), + font = (extract_font_name(S.month.font),1,0)) + num_pages_written += 1 + page.end_page() + if num_pages_written < num_pages: + page.new_page() diff --git a/layouts/classic.py b/layouts/classic.py index bad5b58..89b0d5e 100644 --- a/layouts/classic.py +++ b/layouts/classic.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # callirhoe - high quality calendar rendering -# Copyright (C) 2012 George M. Tzoumas +# Copyright (C) 2012-2013 George M. Tzoumas # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -48,14 +48,8 @@ parser.add_option("--long-daycells", action="store_const", const=0.0, dest="shor help="force use of only long daycells") parser.add_option("--short-daycells", action="store_const", const=1.0e6, dest="short_daycell_ratio", help="force use of only short daycells") -parser.add_option("--bar", action="store_const", const=1.0e6, dest="month_bar_ratio", - help="force month drawing in bar mode") -parser.add_option("--matrix", action="store_const", const=0, dest="month_bar_ratio", - help="force month drawing in matrix mode") parser.add_option("--short-daycell-ratio", type="float", default=2.5, help="ratio threshold for day cells below which short version is drawn [%default]") -parser.add_option("--month-bar-ratio", type="float", default=0.7, - help="ratio threshold for month box, below which bar is drawn [%default]") parser.add_option("--no-footer", action="store_true", default=False, help="disable footer line (with year and rendered-by message)") parser.add_option("--symmetric", action="store_true", default=False, @@ -84,7 +78,7 @@ def _weekrows_of_month(year, month): return 5 def _draw_day_cell_short(cr, rect, day, header, footer, theme, show_day_name): - S,G = theme + S,G,L = theme x, y, w, h = rect day_of_month, day_of_week = day draw_box(cr, rect, S.frame, S.bg, mm_to_dots(S.frame_thickness)) @@ -99,7 +93,7 @@ def _draw_day_cell_short(cr, rect, day, header, footer, theme, show_day_name): align = (2,valign), font = S.font, measure = "88") # draw name of day if show_day_name: - draw_str(cr, text = calendar.day_name[day_of_week][0], rect = Rdow, stretch = -1, stroke_rgba = S.fg, + draw_str(cr, text = L.day_name[day_of_week][0], rect = Rdow, stretch = -1, stroke_rgba = S.fg, align = (2,valign), font = S.font, measure = "88") # draw header if header: @@ -113,7 +107,7 @@ def _draw_day_cell_short(cr, rect, day, header, footer, theme, show_day_name): font = S.footer_font) def _draw_day_cell_long(cr, rect, day, header, footer, theme, show_day_name): - S,G = theme + S,G,L = theme x, y, w, h = rect day_of_month, day_of_week = day draw_box(cr, rect, S.frame, S.bg, mm_to_dots(S.frame_thickness)) @@ -129,7 +123,7 @@ def _draw_day_cell_long(cr, rect, day, header, footer, theme, show_day_name): align = (2,valign), font = S.font, measure = "88") # draw name of day if show_day_name: - draw_str(cr, text = calendar.day_name[day_of_week], rect = Rdow, stretch = -1, stroke_rgba = S.fg, + draw_str(cr, text = L.day_name[day_of_week], rect = Rdow, stretch = -1, stroke_rgba = S.fg, align = (0,valign), font = S.font, measure = "M") Rh, Rf = rect_vsplit(Rhf, *G.hf_vsplit) # draw header @@ -147,15 +141,15 @@ def draw_day_cell(cr, rect, day, header, footer, theme, show_day_name, short_thr else: _draw_day_cell_long(cr, rect, day, header, footer, theme, show_day_name) -def draw_month_matrix(cr, rect, month, year, theme, holiday_provider, daycell_thres): - S,G = theme +def draw_month(cr, rect, month, year, theme, holiday_provider, daycell_thres = 2.5): + S,G,L = theme apply_rect(cr, rect, G.month.sloppy_dx, G.month.sloppy_dy, G.month.sloppy_rot) day, span = calendar.monthrange(year, month) weekrows = 6 if G.month.symmetric else _weekrows_of_month(year, month) dom = -day + 1; - wmeasure = 'A'*max(map(len,calendar.day_name)) - mmeasure = 'A'*max(map(len,calendar.month_name)) + wmeasure = 'A'*max(map(len,L.day_name)) + mmeasure = 'A'*max(map(len,L.month_name)) if options.month_with_year: mmeasure += 'A'*(len(str(year))+1) @@ -178,7 +172,7 @@ def draw_month_matrix(cr, rect, month, year, theme, holiday_provider, daycell_th fill_rgba = S.dom.bg if col < 5 else S.dom_weekend.bg, stroke_width = mm_to_dots(S.dow.frame_thickness)) R_text = rect_rel_scale(R, 1, 0.5) - draw_str(cr, text = calendar.day_name[col], rect = R_text, stretch = -1, stroke_rgba = S.dow.fg, + draw_str(cr, text = L.day_name[col], rect = R_text, stretch = -1, stroke_rgba = S.dow.fg, align = (2,0), font = S.dow.font, measure = wmeasure) # draw day cells @@ -189,7 +183,7 @@ def draw_month_matrix(cr, rect, month, year, theme, holiday_provider, daycell_th holiday_tuple = holiday_provider(year, month, dom, col) day_style = holiday_tuple[2] draw_day_cell(cr, rect = R, day = (dom, col), - header = holiday_tuple[0], footer = holiday_tuple[1], theme = (day_style, G.dom), show_day_name = False, + header = holiday_tuple[0], footer = holiday_tuple[1], theme = (day_style, G.dom, L), show_day_name = False, short_thres = daycell_thres) else: day_style = S.dom_weekend if col >= 5 else S.dom @@ -209,70 +203,12 @@ def draw_month_matrix(cr, rect, month, year, theme, holiday_provider, daycell_th if S.month.text_shadow: f = S.month.text_shadow_size mshad = (f,-f) if G.landscape else (f,f) - title_str = calendar.month_name[month] + title_str = L.month_name[month] if options.month_with_year: title_str += ' ' + str(year) draw_str(cr, text = title_str, rect = R_text, stretch = -1, stroke_rgba = mcolor_fg, align = (2,0), font = S.month.font, measure = mmeasure, shadow = mshad) cr.restore() -def draw_month_bar(cr, rect, month, year, theme, holiday_provider, daycell_thres): - S,G = theme - apply_rect(cr, rect, G.month.sloppy_dx, G.month.sloppy_dy, G.month.sloppy_rot) - - day, span = calendar.monthrange(year, month) - mmeasure = 'A'*max(map(len,calendar.month_name)) - if options.month_with_year: - mmeasure += 'A'*(len(str(year))+1) - - rows = 31 if G.month.symmetric else span - grid = VLayout(rect_from_origin(rect), 32) # title bar always symmetric - dom_grid = VLayout(grid.item_span(31,1), rows) - - # draw box shadow - if S.month.box_shadow: - f = S.month.box_shadow_size - shad = (f,-f) if G.landscape else (f,f) - draw_shadow(cr, rect_from_origin(rect), shad) - - # draw day cells - for dom in range(1,rows+1): - R = dom_grid.item(dom-1) - if dom <= span: - holiday_tuple = holiday_provider(year, month, dom, day) - day_style = holiday_tuple[2] - draw_day_cell(cr, rect = R, day = (dom, day), header = holiday_tuple[0], footer = holiday_tuple[1], - theme = (day_style, G.dom), show_day_name = True, short_thres = daycell_thres) - else: - day_style = S.dom - draw_box(cr, rect = R, stroke_rgba = day_style.frame, fill_rgba = day_style.bg, - stroke_width = mm_to_dots(day_style.frame_thickness)) - day = (day + 1) % 7 - - # draw month title (name) - mcolor = S.month.color_map_bg[year%2][month] - mcolor_fg = S.month.color_map_fg[year%2][month] - R_mb = grid.item(0) - draw_box(cr, rect = R_mb, stroke_rgba = S.month.frame, fill_rgba = mcolor, - stroke_width = mm_to_dots(S.month.frame_thickness)) # title box - draw_box(cr, rect = rect_from_origin(rect), stroke_rgba = S.month.frame, fill_rgba = (), - stroke_width = mm_to_dots(S.month.frame_thickness)) # full box - R_text = rect_rel_scale(R_mb, 1, 0.5) - mshad = None - if S.month.text_shadow: - f = S.month.text_shadow_size - mshad = (f,-f) if G.landscape else (f,f) - title_str = calendar.month_name[month] - if options.month_with_year: title_str += ' ' + str(year) - draw_str(cr, text = title_str, rect = R_text, stretch = -1, stroke_rgba = mcolor_fg, - align = (2,0), font = S.month.font, measure = mmeasure, shadow = mshad) - cr.restore() - -def draw_month(cr, rect, month, year, theme, holiday_provider, bar_thres = 0.7, daycell_thres = 2.5): - if rect_ratio(rect) >= bar_thres: - draw_month_matrix(cr, rect, month, year, theme, holiday_provider, daycell_thres) - else: - draw_month_bar(cr, rect, month, year, theme, holiday_provider, daycell_thres) - #1 1 1 #2 2 1 #3 3 1 @@ -292,7 +228,7 @@ def draw_month(cr, rect, month, year, theme, holiday_provider, bar_thres = 0.7, #cols = 0 def draw_calendar(Outfile, Year, Month, MonthSpan, Theme, holiday_provider, version_string): - S,G = Theme + S,G,L = Theme rows, cols = options.rows, options.cols if options.symmetric: @@ -306,15 +242,6 @@ def draw_calendar(Outfile, Year, Month, MonthSpan, Theme, holiday_provider, vers S.month.color_map_bg = (S.month.color_map_bg[1], S.month.color_map_bg[0]) S.month.color_map_fg = (S.month.color_map_fg[1], S.month.color_map_fg[0]) - if options.long_daynames: - calendar.day_name = calendar.long_day_name - else: - calendar.day_name = calendar.short_day_name - if options.short_monthnames: - calendar.month_name = calendar.short_month_name - else: - calendar.month_name = calendar.long_month_name - try: page = PageWriter(Outfile, G.landscape, G.pagespec, G.border, not options.opaque) except InvalidFormat as e: @@ -379,7 +306,7 @@ def draw_calendar(Outfile, Year, Month, MonthSpan, Theme, holiday_provider, vers k = len(p) - num_placed - 1 if z_order == "decreasing" else num_placed draw_month(page.cr, grid.item_seq(k, options.grid_order == "column"), month=m, year=y, theme = Theme, holiday_provider = holiday_provider, - bar_thres = options.month_bar_ratio, daycell_thres = options.short_daycell_ratio) + daycell_thres = options.short_daycell_ratio) num_placed += 1 if (y > yy[-1]): yy.append(y) if not options.month_with_year and not options.no_footer: diff --git a/lib/holiday.py b/lib/holiday.py index d5125f5..b1c833f 100644 --- a/lib/holiday.py +++ b/lib/holiday.py @@ -55,10 +55,11 @@ class Holiday(object): Properties: header: string for header footer: string for footer - flags : bit combination of {OFF=1, MULTI=2} + flags : bit combination of {OFF=1, MULTI=2, REMINDER=4} OFF: day off (real holiday) MULTI: multi-day event (used to mark long day ranges, not necessarily holidays) + REMINDER: do not mark the day as holiday Remarks: Rendering style is considered in the following order: @@ -68,6 +69,7 @@ class Holiday(object): """ OFF = 1 MULTI = 2 + REMINDER = 4 def __init__(self, header = [], footer = [], flags_str = None): self.header_list = self._strip_empty(header) self.footer_list = self._strip_empty(footer) @@ -95,6 +97,8 @@ class Holiday(object): for s in fs: if s == 'off': val |= Holiday.OFF elif s == 'multi': val |= Holiday.MULTI + # allow for prefix abbrev. + elif 'reminder'.startswith(s): val |= Holiday.REMINDER return val def _strip_empty(self, sl): @@ -118,7 +122,7 @@ def _decode_date_str(ddef): raise ValueError("invalid date definition '%s'" % ddef) class HolidayProvider(object): - def __init__(self, s_normal, s_weekend, s_holiday, s_weekend_holiday, s_multi, s_weekend_multi): + def __init__(self, s_normal, s_weekend, s_holiday, s_weekend_holiday, s_multi, s_weekend_multi, verbose=True): self.annual = dict() # key = (d,m) self.monthly = dict() # key = d self.fixed = dict() # key = date() @@ -133,6 +137,7 @@ class HolidayProvider(object): self.s_weekend_holiday = s_weekend_holiday self.s_multi = s_multi self.s_weekend_multi = s_weekend_multi + self.verbose = verbose def parse_day_record(self, fields): """return tuple (etype,ddef,footer,header,flags) @@ -184,11 +189,17 @@ class HolidayProvider(object): def multi_holiday_tuple(self, date1, date2, header, footer, flags): """returns Holiday objects for (beginning, end, first_dom, rest)""" if header: - header_tuple = (header+'..', '..'+header, '..'+header+'..', None) + if self.verbose: + header_tuple = (header+'..', '..'+header, '..'+header+'..', None) + else: + header_tuple = (header, None, header, None) else: header_tuple = (None, None, None, None) if footer: - footer_tuple = (footer+'..', '..'+footer, '..'+footer+'..', None) + if self.verbose: + footer_tuple = (footer+'..', '..'+footer, '..'+footer+'..', None) + else: + footer_tuple = (footer, None, footer, None) else: footer_tuple = (None, None, None, None) return tuple(map(lambda k: Holiday([header_tuple[k]], [footer_tuple[k]], flags), diff --git a/lib/xcairo.py b/lib/xcairo.py index c6e239c..15acc4f 100644 --- a/lib/xcairo.py +++ b/lib/xcairo.py @@ -172,6 +172,17 @@ def draw_shadow(cr, rect, thickness = None, shadow_color = (0,0,0,0.3)): set_color(cr, shadow_color) cr.close_path(); cr.fill(); +def draw_line(cr, rect, stroke_rgba = None, stroke_width = 1.0): + if (stroke_width <= 0): return + x, y, w, h = rect + cr.move_to(x, y) + cr.rel_line_to(w, h) + cr.close_path() + if stroke_rgba: + set_color(cr, stroke_rgba) + cr.set_line_width(stroke_width) + cr.stroke() + def draw_box(cr, rect, stroke_rgba = None, fill_rgba = None, stroke_width = 1.0, shadow = None): if (stroke_width <= 0): return draw_shadow(cr, rect, shadow) @@ -202,6 +213,10 @@ def draw_str(cr, text, rect, stretch = -1, stroke_rgba = None, align = (2,0), bb if measure is None: measure = text te = cr.text_extents(measure) mw, mh = te[2], te[3] + if mw < 5: + mw = 5. + if mh < 5: + mh = 5. ratio, tratio = w*1.0/h, mw*1.0/mh; xratio, yratio = mw*1.0/w, mh*1.0/h; if stretch < 0: stretch = 1 if xratio >= yratio else 2 From 855d107cd1db8cb4f170c0a926f0fee01e196877 Mon Sep 17 00:00:00 2001 From: "geortz@gmail.com" Date: Mon, 10 Mar 2014 00:29:49 +0000 Subject: [PATCH 10/17] layout separation, common code [WIP] git-svn-id: https://callirhoe.googlecode.com/svn/branches/next@42 81c8bb96-aa45-f2e2-0eef-c4fa4a15c6df --- callirhoe.py | 6 ++- layouts/classic.py | 125 +++++---------------------------------------- lib/plugin.py | 6 ++- 3 files changed, 22 insertions(+), 115 deletions(-) diff --git a/callirhoe.py b/callirhoe.py index 9350421..5fee2cd 100755 --- a/callirhoe.py +++ b/callirhoe.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # callirhoe - high quality calendar rendering -# Copyright (C) 2012-2013 George M. Tzoumas +# Copyright (C) 2012-2014 George M. Tzoumas # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -179,7 +179,9 @@ for x in argv2: if not Layout.parser.has_option(x): parser.error("invalid option %s; use --help (-h) or --layout-help (-?) to see available options" % x) -(Layout.options,largs) = Layout.parser.parse_args(argv2) +(loptions,largs) = Layout.parser.parse_args(argv2) +Layout.setoptions(loptions) + if options.layouthelp: #print "Help for layout:", options.layout Layout.parser.print_help() diff --git a/layouts/classic.py b/layouts/classic.py index 89b0d5e..4aa8c04 100644 --- a/layouts/classic.py +++ b/layouts/classic.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # callirhoe - high quality calendar rendering -# Copyright (C) 2012-2013 George M. Tzoumas +# Copyright (C) 2012-2014 George M. Tzoumas # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -21,54 +21,19 @@ from lib.xcairo import * from lib.geom import * from math import floor, ceil, sqrt import calendar -import optparse import sys from datetime import date, timedelta -parser = optparse.OptionParser(usage="%prog (...) --layout classic [options] (...)",add_help_option=False) -parser.add_option("--rows", type="int", default=0, help="force grid rows [%default]") -parser.add_option("--cols", type="int", default=0, - help="force grid columns [%default]; if ROWS and COLS are both non-zero, " - "calendar will span multiple pages as needed; if one value is zero, it " - "will be computed automatically in order to fill exactly 1 page") -parser.add_option("--grid-order", choices=["row","column"],default="row", - help="either `row' or `column' to set grid placing order row-wise or column-wise [%default]") -parser.add_option("--z-order", choices=["auto", "increasing", "decreasing"], default="auto", - help="either `increasing' or `decreasing' to set whether next month (in grid order) " - "lies above or below the previously drawn month; this affects shadow casting, " - "since rendering is always performed in increasing z-order; specifying `auto' " - "selects increasing order if and only if sloppy boxes are enabled [%default]") -parser.add_option("--month-with-year", action="store_true", default=False, - help="displays year together with month name, e.g. January 1980; suppresses year from footer line") -parser.add_option("--short-monthnames", action="store_true", default=False, - help="user the short version of month names (defined in language file) [%default]") -parser.add_option("--long-daynames", action="store_true", default=False, - help="user the long version of day names (defined in language file) [%default]") -parser.add_option("--long-daycells", action="store_const", const=0.0, dest="short_daycell_ratio", - help="force use of only long daycells") -parser.add_option("--short-daycells", action="store_const", const=1.0e6, dest="short_daycell_ratio", - help="force use of only short daycells") -parser.add_option("--short-daycell-ratio", type="float", default=2.5, - help="ratio threshold for day cells below which short version is drawn [%default]") -parser.add_option("--no-footer", action="store_true", default=False, - help="disable footer line (with year and rendered-by message)") -parser.add_option("--symmetric", action="store_true", default=False, - help="force symmetric mode (equivalent to --geom-var=month.symmetric=1). " - "In symmetric mode, day cells are equally sized and all month boxes contain " - "the same number of (possibly empty) cells, independently of how many days or " - "weeks per month. In asymmetric mode, empty rows are eliminated, by slightly " - "resizing day cells, in order to have uniform month boxes.") -parser.add_option("--padding", type="float", default=None, - help="set month box padding (equivalent to --geom-var=month.padding=PADDING); " - "month bars look better with smaller padding, while matrix mode looks better with " - "larger padding") -parser.add_option("--no-shadow", action="store_true", default=None, - help="disable box shadows") -parser.add_option("--opaque", action="store_true", default=False, - help="make background opaque (white fill)") -parser.add_option("--swap-colors", action="store_true", default=None, - help="swap month colors for even/odd years") +import _base +parser = _base.get_parser() + +options = None + +def setoptions(opt): + global options + options = opt + _base.options = opt def _weekrows_of_month(year, month): day,span = calendar.monthrange(year, month) @@ -77,70 +42,6 @@ def _weekrows_of_month(year, month): if day == 6 and span >= 30: return 6 return 5 -def _draw_day_cell_short(cr, rect, day, header, footer, theme, show_day_name): - S,G,L = theme - x, y, w, h = rect - day_of_month, day_of_week = day - draw_box(cr, rect, S.frame, S.bg, mm_to_dots(S.frame_thickness)) - R = rect_rel_scale(rect, G.size[0], G.size[1]) - if show_day_name: - Rdom, Rdow = rect_hsplit(R, *G.mw_split) - else: - Rdom = R - valign = 0 if show_day_name else 2 - # draw day of month (number) - draw_str(cr, text = str(day_of_month), rect = Rdom, stretch = -1, stroke_rgba = S.fg, - align = (2,valign), font = S.font, measure = "88") - # draw name of day - if show_day_name: - draw_str(cr, text = L.day_name[day_of_week][0], rect = Rdow, stretch = -1, stroke_rgba = S.fg, - align = (2,valign), font = S.font, measure = "88") - # draw header - if header: - R = rect_rel_scale(rect, G.header_size[0], G.header_size[1], 0, -1.0 + G.header_align) - draw_str(cr, text = header, rect = R, stretch = -1, stroke_rgba = S.header, - font = S.header_font) # , measure = "MgMgMgMgMgMg" - # draw footer - if footer: - R = rect_rel_scale(rect, G.footer_size[0], G.footer_size[1], 0, 1.0 - G.footer_align) - draw_str(cr, text = footer, rect = R, stretch = -1, stroke_rgba = S.footer, - font = S.footer_font) - -def _draw_day_cell_long(cr, rect, day, header, footer, theme, show_day_name): - S,G,L = theme - x, y, w, h = rect - day_of_month, day_of_week = day - draw_box(cr, rect, S.frame, S.bg, mm_to_dots(S.frame_thickness)) - R1, Rhf = rect_hsplit(rect, *G.hf_hsplit) - if show_day_name: - R = rect_rel_scale(R1, G.size[2], G.size[3]) - Rdom, Rdow = rect_hsplit(R, *G.mw_split) - else: - Rdom = rect_rel_scale(R1, G.size[0], G.size[1]) - valign = 0 if show_day_name else 2 - # draw day of month (number) - draw_str(cr, text = str(day_of_month), rect = Rdom, stretch = -1, stroke_rgba = S.fg, - align = (2,valign), font = S.font, measure = "88") - # draw name of day - if show_day_name: - draw_str(cr, text = L.day_name[day_of_week], rect = Rdow, stretch = -1, stroke_rgba = S.fg, - align = (0,valign), font = S.font, measure = "M") - Rh, Rf = rect_vsplit(Rhf, *G.hf_vsplit) - # draw header - if header: - draw_str(cr, text = header, rect = Rh, stretch = -1, stroke_rgba = S.header, align = (1,2), - font = S.header_font) - # draw footer - if footer: - draw_str(cr, text = footer, rect = Rf, stretch = -1, stroke_rgba = S.footer, align = (1,2), - font = S.footer_font) - -def draw_day_cell(cr, rect, day, header, footer, theme, show_day_name, short_thres): - if rect_ratio(rect) < short_thres: - _draw_day_cell_short(cr, rect, day, header, footer, theme, show_day_name) - else: - _draw_day_cell_long(cr, rect, day, header, footer, theme, show_day_name) - def draw_month(cr, rect, month, year, theme, holiday_provider, daycell_thres = 2.5): S,G,L = theme apply_rect(cr, rect, G.month.sloppy_dx, G.month.sloppy_dy, G.month.sloppy_rot) @@ -182,9 +83,9 @@ def draw_month(cr, rect, month, year, theme, holiday_provider, daycell_thres = 2 if dom > 0 and dom <= span: holiday_tuple = holiday_provider(year, month, dom, col) day_style = holiday_tuple[2] - draw_day_cell(cr, rect = R, day = (dom, col), - header = holiday_tuple[0], footer = holiday_tuple[1], theme = (day_style, G.dom, L), show_day_name = False, - short_thres = daycell_thres) + dcell = _base.DayCell(day = (dom, col), header = holiday_tuple[0], footer = holiday_tuple[1], + theme = (day_style, G.dom, L), show_day_name = False) + dcell.draw(cr, R, daycell_thres) else: day_style = S.dom_weekend if col >= 5 else S.dom draw_box(cr, rect = R, stroke_rgba = day_style.frame, fill_rgba = day_style.bg, diff --git a/lib/plugin.py b/lib/plugin.py index 6dc5554..10bf149 100644 --- a/lib/plugin.py +++ b/lib/plugin.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # callirhoe - high quality calendar rendering -# Copyright (C) 2012 George M. Tzoumas +# Copyright (C) 2012-2014 George M. Tzoumas # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -33,6 +33,10 @@ def available_files(parent, dir, fmatch = ""): for x in glob.glob(pattern): basex = os.path.basename(x) if basex == "__init__.py": good = True + elif basex.startswith('_'): + # ignore files aimed for internal use + # safer than [a-z]-style matching... + continue else: base = os.path.splitext(basex)[0] if base and ((not fmatch) or (fmatch == base)): res.append((base,parent)) From 1bf1441423537810e25c1a98e0b4dd23ae590422 Mon Sep 17 00:00:00 2001 From: "geortz@gmail.com" Date: Fri, 9 May 2014 11:17:44 +0000 Subject: [PATCH 11/17] added forgotten base file (for layout separation) added generic holidays file git-svn-id: https://callirhoe.googlecode.com/svn/branches/next@43 81c8bb96-aa45-f2e2-0eef-c4fa4a15c6df --- holidays/generic_holidays.EN.dat | 20 +++++ layouts/_base.py | 142 +++++++++++++++++++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 holidays/generic_holidays.EN.dat create mode 100644 layouts/_base.py diff --git a/holidays/generic_holidays.EN.dat b/holidays/generic_holidays.EN.dat new file mode 100644 index 0000000..fdb84f1 --- /dev/null +++ b/holidays/generic_holidays.EN.dat @@ -0,0 +1,20 @@ +# type|DATE*span|footer|header|flags +# type|DATE1-DATE2|footer|header|flags +# type|DATE|footer|header|flags +# +# type: +# d: event occurs annually fixed day/month: MMDD +# d: event occurs monthly, fixed day: DD +# d: fixed day/month/year combination (e.g. deadline, trip, etc.): YYYYMMDD +# oe: Orthodox Easter-dependent holiday, annually +# ge: Georgios' name day, Orthodox Easter dependent holiday, annually +# ce: Catholic Easter holiday +# +# DATE*span and DATE1-DATE2 supported only for YYYYMMDD +# flags = {off, multi} + +d|0101||New year's|off +d|0501||Labour day|off +d|1225||Christmas|off +ce|0||Easter|off +oe|0||Orthodox Easter|off diff --git a/layouts/_base.py b/layouts/_base.py new file mode 100644 index 0000000..804b598 --- /dev/null +++ b/layouts/_base.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +# callirhoe - high quality calendar rendering +# Copyright (C) 2012-2014 George M. Tzoumas + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see http://www.gnu.org/licenses/ + +# --- layouts._base --- + +import optparse +from lib.xcairo import * +from lib.geom import * + +def get_parser(): + parser = optparse.OptionParser(usage="%prog (...) --layout classic [options] (...)",add_help_option=False) + parser.add_option("--rows", type="int", default=0, help="force grid rows [%default]") + parser.add_option("--cols", type="int", default=0, + help="force grid columns [%default]; if ROWS and COLS are both non-zero, " + "calendar will span multiple pages as needed; if one value is zero, it " + "will be computed automatically in order to fill exactly 1 page") + parser.add_option("--grid-order", choices=["row","column"],default="row", + help="either `row' or `column' to set grid placing order row-wise or column-wise [%default]") + parser.add_option("--z-order", choices=["auto", "increasing", "decreasing"], default="auto", + help="either `increasing' or `decreasing' to set whether next month (in grid order) " + "lies above or below the previously drawn month; this affects shadow casting, " + "since rendering is always performed in increasing z-order; specifying `auto' " + "selects increasing order if and only if sloppy boxes are enabled [%default]") + parser.add_option("--month-with-year", action="store_true", default=False, + help="displays year together with month name, e.g. January 1980; suppresses year from footer line") + parser.add_option("--short-monthnames", action="store_true", default=False, + help="user the short version of month names (defined in language file) [%default]") + parser.add_option("--long-daynames", action="store_true", default=False, + help="user the long version of day names (defined in language file) [%default]") + parser.add_option("--long-daycells", action="store_const", const=0.0, dest="short_daycell_ratio", + help="force use of only long daycells") + parser.add_option("--short-daycells", action="store_const", const=1.0e6, dest="short_daycell_ratio", + help="force use of only short daycells") + parser.add_option("--short-daycell-ratio", type="float", default=2.5, + help="ratio threshold for day cells below which short version is drawn [%default]") + parser.add_option("--no-footer", action="store_true", default=False, + help="disable footer line (with year and rendered-by message)") + parser.add_option("--symmetric", action="store_true", default=False, + help="force symmetric mode (equivalent to --geom-var=month.symmetric=1). " + "In symmetric mode, day cells are equally sized and all month boxes contain " + "the same number of (possibly empty) cells, independently of how many days or " + "weeks per month. In asymmetric mode, empty rows are eliminated, by slightly " + "resizing day cells, in order to have uniform month boxes.") + parser.add_option("--padding", type="float", default=None, + help="set month box padding (equivalent to --geom-var=month.padding=PADDING); " + "month bars look better with smaller padding, while matrix mode looks better with " + "larger padding") + parser.add_option("--no-shadow", action="store_true", default=None, + help="disable box shadows") + parser.add_option("--opaque", action="store_true", default=False, + help="make background opaque (white fill)") + parser.add_option("--swap-colors", action="store_true", default=None, + help="swap month colors for even/odd years") + return parser + + +class DayCell(object): + def __init__(day, header, footer, theme, show_day_name): + self.day = day + self.header = header + self.footer = footer + self.theme = theme + self.show_day_name = show_day_name + + def _draw_short(self, cr, rect): + S,G,L = self.theme + x, y, w, h = rect + day_of_month, day_of_week = self.day + draw_box(cr, rect, S.frame, S.bg, mm_to_dots(S.frame_thickness)) + R = rect_rel_scale(rect, G.size[0], G.size[1]) + if self.show_day_name: + Rdom, Rdow = rect_hsplit(R, *G.mw_split) + else: + Rdom = R + valign = 0 if self.show_day_name else 2 + # draw day of month (number) + draw_str(cr, text = str(day_of_month), rect = Rdom, stretch = -1, stroke_rgba = S.fg, + align = (2,valign), font = S.font, measure = "88") + # draw name of day + if self.show_day_name: + draw_str(cr, text = L.day_name[day_of_week][0], rect = Rdow, stretch = -1, stroke_rgba = S.fg, + align = (2,valign), font = S.font, measure = "88") + # draw header + if self.header: + R = rect_rel_scale(rect, G.header_size[0], G.header_size[1], 0, -1.0 + G.header_align) + draw_str(cr, text = self.header, rect = R, stretch = -1, stroke_rgba = S.header, + font = S.header_font) # , measure = "MgMgMgMgMgMg" + # draw footer + if self.footer: + R = rect_rel_scale(rect, G.footer_size[0], G.footer_size[1], 0, 1.0 - G.footer_align) + draw_str(cr, text = self.footer, rect = R, stretch = -1, stroke_rgba = S.footer, + font = S.footer_font) + + def _draw_long(self, cr, rect): + S,G,L = self.theme + x, y, w, h = rect + day_of_month, day_of_week = self.day + draw_box(cr, rect, S.frame, S.bg, mm_to_dots(S.frame_thickness)) + R1, Rhf = rect_hsplit(rect, *G.hf_hsplit) + if self.show_day_name: + R = rect_rel_scale(R1, G.size[2], G.size[3]) + Rdom, Rdow = rect_hsplit(R, *G.mw_split) + else: + Rdom = rect_rel_scale(R1, G.size[0], G.size[1]) + valign = 0 if self.show_day_name else 2 + # draw day of month (number) + draw_str(cr, text = str(day_of_month), rect = Rdom, stretch = -1, stroke_rgba = S.fg, + align = (2,valign), font = S.font, measure = "88") + # draw name of day + if self.show_day_name: + draw_str(cr, text = L.day_name[day_of_week], rect = Rdow, stretch = -1, stroke_rgba = S.fg, + align = (0,valign), font = S.font, measure = "M") + Rh, Rf = rect_vsplit(Rhf, *G.hf_vsplit) + # draw header + if self.header: + draw_str(cr, text = self.header, rect = Rh, stretch = -1, stroke_rgba = S.header, align = (1,2), + font = S.header_font) + # draw footer + if self.footer: + draw_str(cr, text = self.footer, rect = Rf, stretch = -1, stroke_rgba = S.footer, align = (1,2), + font = S.footer_font) + + def draw(self, cr, rect, short_thres): + if rect_ratio(rect) < short_thres: + _draw_short(cr, rect) + else: + _draw_long(cr, rect) + From 627f1d9c47b5cd309d2ce5ac0aef2b3b4a6e03f9 Mon Sep 17 00:00:00 2001 From: "geortz@gmail.com" Date: Fri, 9 May 2014 14:01:28 +0000 Subject: [PATCH 12/17] fixed common code base should now be runnable :) git-svn-id: https://callirhoe.googlecode.com/svn/branches/next@44 81c8bb96-aa45-f2e2-0eef-c4fa4a15c6df --- callirhoe.py | 9 +- layouts/_base.py | 137 ++++++++++++++++++- layouts/bars.py | 322 ++++++++------------------------------------- layouts/classic.py | 248 +++++++++------------------------- 4 files changed, 255 insertions(+), 461 deletions(-) diff --git a/callirhoe.py b/callirhoe.py index 5fee2cd..32938c5 100755 --- a/callirhoe.py +++ b/callirhoe.py @@ -180,7 +180,6 @@ for x in argv2: parser.error("invalid option %s; use --help (-h) or --layout-help (-?) to see available options" % x) (loptions,largs) = Layout.parser.parse_args(argv2) -Layout.setoptions(loptions) if options.layouthelp: #print "Help for layout:", options.layout @@ -271,14 +270,16 @@ if options.holidays: for f in options.holidays: hprovider.load_holiday_file(f) -if options.long_daynames: +if loptions.long_daynames: Language.day_name = Language.long_day_name else: Language.day_name = Language.short_day_name -if options.short_monthnames: +if loptions.short_monthnames: Language.month_name = Language.short_month_name else: Language.month_name = Language.long_month_name -Layout.draw_calendar(Outfile, Year, Month, MonthSpan, (Style,Geometry,Language), hprovider, _version) +renderer = Layout.CalendarRenderer(Outfile, Year, Month, MonthSpan, + (Style,Geometry,Language), hprovider, _version, loptions) +renderer.render() diff --git a/layouts/_base.py b/layouts/_base.py index 804b598..ee726b1 100644 --- a/layouts/_base.py +++ b/layouts/_base.py @@ -20,9 +20,11 @@ import optparse from lib.xcairo import * from lib.geom import * +from math import floor, ceil, sqrt -def get_parser(): - parser = optparse.OptionParser(usage="%prog (...) --layout classic [options] (...)",add_help_option=False) +def get_parser(layout_name): + lname = layout_name.split(".")[1] + parser = optparse.OptionParser(usage="%prog (...) --layout " + lname + " [options] (...)",add_help_option=False) parser.add_option("--rows", type="int", default=0, help="force grid rows [%default]") parser.add_option("--cols", type="int", default=0, help="force grid columns [%default]; if ROWS and COLS are both non-zero, " @@ -69,7 +71,7 @@ def get_parser(): class DayCell(object): - def __init__(day, header, footer, theme, show_day_name): + def __init__(self, day, header, footer, theme, show_day_name): self.day = day self.header = header self.footer = footer @@ -136,7 +138,132 @@ class DayCell(object): def draw(self, cr, rect, short_thres): if rect_ratio(rect) < short_thres: - _draw_short(cr, rect) + self._draw_short(cr, rect) else: - _draw_long(cr, rect) + self._draw_long(cr, rect) + +class CalendarRenderer(object): + def __init__(self, Outfile, Year, Month, MonthSpan, Theme, holiday_provider, version_string, options): + self.Outfile = Outfile + self.Year = Year + self.Month = Month + self.MonthSpan = MonthSpan + self.Theme = Theme + self.holiday_provider = holiday_provider + self.version_string = version_string + self.options = options + + def _draw_month(self, cr, rect, month, year, daycell_thres): + raise NotImplementedError("base _draw_month() should be overridden") + +#1 1 1 +#2 2 1 +#3 3 1 + +#4 2 2 +#5 3 2 +#6 3 2 +#7 4 2 +#8 4 2 + +#9 3 3 +#10 4 3 +#11 4 3 +#12 4 3 + +#rows = 0 +#cols = 0 + def render(self): + S,G,L = self.Theme + rows, cols = self.options.rows, self.options.cols + + if self.options.symmetric: + G.month.symmetric = True + if self.options.padding is not None: + G.month.padding = self.options.padding + if self.options.no_shadow == True: + S.month.box_shadow = False + if self.Year % 2: self.options.swap_colors = not self.options.swap_colors + if self.options.swap_colors: + S.month.color_map_bg = (S.month.color_map_bg[1], S.month.color_map_bg[0]) + S.month.color_map_fg = (S.month.color_map_fg[1], S.month.color_map_fg[0]) + + try: + page = PageWriter(self.Outfile, G.landscape, G.pagespec, G.border, not self.options.opaque) + except InvalidFormat as e: + print >> sys.stderr, "invalid output format", e.args[0] + sys.exit(1) + + if rows == 0 and cols == 0: + # if MonthSpan < 4: + # cols = 1; rows = MonthSpan + # elif MonthSpan < 9: + # cols = 2; rows = int(math.ceil(MonthSpan/2.0)) + # else: + # TODO: improve this heuristic + cols = int(floor(sqrt(self.MonthSpan))) + rows = cols + if rows*cols < self.MonthSpan: rows += 1 + if rows*cols < self.MonthSpan: rows += 1 + if rows*cols < self.MonthSpan: cols += 1; rows -= 1 + if G.landscape: rows, cols = cols, rows + elif rows == 0: + rows = int(ceil(self.MonthSpan*1.0/cols)) + elif cols == 0: + cols = int(ceil(self.MonthSpan*1.0/rows)) + G.landscape = page.landscape # PNG is pseudo-landscape (portrait with width>height) + + if not self.options.no_footer: + V0 = VLayout(page.Text_rect, 40, (1,)*4) + Rcal = V0.item_span(39,0) + Rc = rect_rel_scale(V0.item(39),1,0.5,0,0) + else: + Rcal = page.Text_rect + + grid = GLayout(Rcal, rows, cols, pad = (mm_to_dots(G.month.padding),)*4) + mpp = grid.count() # months per page + num_pages = int(ceil(self.MonthSpan*1.0/mpp)) + cur_month = self.Month + cur_year = self.Year + num_placed = 0 + page_layout = [] + for k in xrange(num_pages): + page_layout.append([]) + for i in xrange(mpp): + page_layout[k].append((cur_month,cur_year)) + num_placed += 1 + cur_month += 1 + if cur_month > 12: cur_month = 1; cur_year += 1 + if num_placed >= self.MonthSpan: break + + num_pages_written = 0 + + z_order = self.options.z_order + if z_order == "auto": + if G.month.sloppy_dx != 0 or G.month.sloppy_dy != 0 or G.month.sloppy_rot != 0: + z_order = "decreasing" + else: + z_order = "increasing" + for p in page_layout: + num_placed = 0 + yy = [p[0][1]] + if z_order == "decreasing": p.reverse() + for (m,y) in p: + k = len(p) - num_placed - 1 if z_order == "decreasing" else num_placed + self._draw_month(page.cr, grid.item_seq(k, self.options.grid_order == "column"), + month=m, year=y, daycell_thres = self.options.short_daycell_ratio) + num_placed += 1 + if (y > yy[-1]): yy.append(y) + if not self.options.month_with_year and not self.options.no_footer: + year_str = str(yy[0]) if yy[0] == yy[-1] else "%s – %s" % (yy[0],yy[-1]) + draw_str(page.cr, text = year_str, rect = Rc, stroke_rgba = (0,0,0,0.5), stretch = -1, + align = (0,0), font = (extract_font_name(S.month.font),0,0)) + if not self.options.no_footer: + draw_str(page.cr, text = "rendered by Callirhoe ver. %s" % self.version_string, + rect = Rc, stroke_rgba = (0,0,0,0.5), stretch = -1, align = (1,0), + font = (extract_font_name(S.month.font),1,0)) + num_pages_written += 1 + page.end_page() + if num_pages_written < num_pages: + page.new_page() diff --git a/layouts/bars.py b/layouts/bars.py index 98820d3..84fc72e 100644 --- a/layouts/bars.py +++ b/layouts/bars.py @@ -19,283 +19,67 @@ from lib.xcairo import * from lib.geom import * -from math import floor, ceil, sqrt import calendar import optparse import sys from datetime import date, timedelta -parser = optparse.OptionParser(usage="%prog (...) --layout classic [options] (...)",add_help_option=False) -parser.add_option("--rows", type="int", default=0, help="force grid rows [%default]") -parser.add_option("--cols", type="int", default=0, - help="force grid columns [%default]; if ROWS and COLS are both non-zero, " - "calendar will span multiple pages as needed; if one value is zero, it " - "will be computed automatically in order to fill exactly 1 page") -parser.add_option("--grid-order", choices=["row","column"],default="row", - help="either `row' or `column' to set grid placing order row-wise or column-wise [%default]") -parser.add_option("--z-order", choices=["auto", "increasing", "decreasing"], default="auto", - help="either `increasing' or `decreasing' to set whether next month (in grid order) " - "lies above or below the previously drawn month; this affects shadow casting, " - "since rendering is always performed in increasing z-order; specifying `auto' " - "selects increasing order if and only if sloppy boxes are enabled [%default]") -parser.add_option("--month-with-year", action="store_true", default=False, - help="displays year together with month name, e.g. January 1980; suppresses year from footer line") -parser.add_option("--short-monthnames", action="store_true", default=False, - help="user the short version of month names (defined in language file) [%default]") -parser.add_option("--long-daynames", action="store_true", default=False, - help="user the long version of day names (defined in language file) [%default]") -parser.add_option("--long-daycells", action="store_const", const=0.0, dest="short_daycell_ratio", - help="force use of only long daycells") -parser.add_option("--short-daycells", action="store_const", const=1.0e6, dest="short_daycell_ratio", - help="force use of only short daycells") -parser.add_option("--short-daycell-ratio", type="float", default=2.5, - help="ratio threshold for day cells below which short version is drawn [%default]") -parser.add_option("--no-footer", action="store_true", default=False, - help="disable footer line (with year and rendered-by message)") -parser.add_option("--symmetric", action="store_true", default=False, - help="force symmetric mode (equivalent to --geom-var=month.symmetric=1). " - "In symmetric mode, day cells are equally sized and all month boxes contain " - "the same number of (possibly empty) cells, independently of how many days or " - "weeks per month. In asymmetric mode, empty rows are eliminated, by slightly " - "resizing day cells, in order to have uniform month boxes.") -parser.add_option("--padding", type="float", default=None, - help="set month box padding (equivalent to --geom-var=month.padding=PADDING); " - "month bars look better with smaller padding, while matrix mode looks better with " - "larger padding") -parser.add_option("--no-shadow", action="store_true", default=None, - help="disable box shadows") -parser.add_option("--opaque", action="store_true", default=False, - help="make background opaque (white fill)") -parser.add_option("--swap-colors", action="store_true", default=None, - help="swap month colors for even/odd years") +import _base +parser = _base.get_parser(__name__) -def _draw_day_cell_short(cr, rect, day, header, footer, theme, show_day_name): - S,G,L = theme - x, y, w, h = rect - day_of_month, day_of_week = day - draw_box(cr, rect, S.frame, S.bg, mm_to_dots(S.frame_thickness)) - R = rect_rel_scale(rect, G.size[0], G.size[1]) - if show_day_name: - Rdom, Rdow = rect_hsplit(R, *G.mw_split) - else: - Rdom = R - valign = 0 if show_day_name else 2 - # draw day of month (number) - draw_str(cr, text = str(day_of_month), rect = Rdom, stretch = -1, stroke_rgba = S.fg, - align = (2,valign), font = S.font, measure = "88") - # draw name of day - if show_day_name: - draw_str(cr, text = L.day_name[day_of_week][0], rect = Rdow, stretch = -1, stroke_rgba = S.fg, - align = (2,valign), font = S.font, measure = "88") - # draw header - if header: - R = rect_rel_scale(rect, G.header_size[0], G.header_size[1], 0, -1.0 + G.header_align) - draw_str(cr, text = header, rect = R, stretch = -1, stroke_rgba = S.header, - font = S.header_font) # , measure = "MgMgMgMgMgMg" - # draw footer - if footer: - R = rect_rel_scale(rect, G.footer_size[0], G.footer_size[1], 0, 1.0 - G.footer_align) - draw_str(cr, text = footer, rect = R, stretch = -1, stroke_rgba = S.footer, - font = S.footer_font) +class CalendarRenderer(_base.CalendarRenderer): + #default thres = 2.5 + def _draw_month(self, cr, rect, month, year, daycell_thres): + S,G,L = self.Theme + apply_rect(cr, rect, G.month.sloppy_dx, G.month.sloppy_dy, G.month.sloppy_rot) -def _draw_day_cell_long(cr, rect, day, header, footer, theme, show_day_name): - S,G,L = theme - x, y, w, h = rect - day_of_month, day_of_week = day - draw_box(cr, rect, S.frame, S.bg, mm_to_dots(S.frame_thickness)) - R1, Rhf = rect_hsplit(rect, *G.hf_hsplit) - if show_day_name: - R = rect_rel_scale(R1, G.size[2], G.size[3]) - Rdom, Rdow = rect_hsplit(R, *G.mw_split) - else: - Rdom = rect_rel_scale(R1, G.size[0], G.size[1]) - valign = 0 if show_day_name else 2 - # draw day of month (number) - draw_str(cr, text = str(day_of_month), rect = Rdom, stretch = -1, stroke_rgba = S.fg, - align = (2,valign), font = S.font, measure = "88") - # draw name of day - if show_day_name: - draw_str(cr, text = L.day_name[day_of_week], rect = Rdow, stretch = -1, stroke_rgba = S.fg, - align = (0,valign), font = S.font, measure = "M") - Rh, Rf = rect_vsplit(Rhf, *G.hf_vsplit) - # draw header - if header: - draw_str(cr, text = header, rect = Rh, stretch = -1, stroke_rgba = S.header, align = (1,2), - font = S.header_font) - # draw footer - if footer: - draw_str(cr, text = footer, rect = Rf, stretch = -1, stroke_rgba = S.footer, align = (1,2), - font = S.footer_font) + day, span = calendar.monthrange(year, month) + mmeasure = 'A'*max(map(len,L.month_name)) + if self.options.month_with_year: + mmeasure += 'A'*(len(str(year))+1) -def draw_day_cell(cr, rect, day, header, footer, theme, show_day_name, short_thres): - if rect_ratio(rect) < short_thres: - _draw_day_cell_short(cr, rect, day, header, footer, theme, show_day_name) - else: - _draw_day_cell_long(cr, rect, day, header, footer, theme, show_day_name) + rows = 31 if G.month.symmetric else span + grid = VLayout(rect_from_origin(rect), 32) # title bar always symmetric + dom_grid = VLayout(grid.item_span(31,1), rows) -def draw_month(cr, rect, month, year, theme, holiday_provider, daycell_thres = 2.5): - S,G,L = theme - apply_rect(cr, rect, G.month.sloppy_dx, G.month.sloppy_dy, G.month.sloppy_rot) - - day, span = calendar.monthrange(year, month) - mmeasure = 'A'*max(map(len,L.month_name)) - if options.month_with_year: - mmeasure += 'A'*(len(str(year))+1) - - rows = 31 if G.month.symmetric else span - grid = VLayout(rect_from_origin(rect), 32) # title bar always symmetric - dom_grid = VLayout(grid.item_span(31,1), rows) - - # draw box shadow - if S.month.box_shadow: - f = S.month.box_shadow_size - shad = (f,-f) if G.landscape else (f,f) - draw_shadow(cr, rect_from_origin(rect), shad) - - # draw day cells - for dom in range(1,rows+1): - R = dom_grid.item(dom-1) - if dom <= span: - holiday_tuple = holiday_provider(year, month, dom, day) - day_style = holiday_tuple[2] - draw_day_cell(cr, rect = R, day = (dom, day), header = holiday_tuple[0], footer = holiday_tuple[1], - theme = (day_style, G.dom, L), show_day_name = True, short_thres = daycell_thres) - else: - day_style = S.dom - draw_box(cr, rect = R, stroke_rgba = day_style.frame, fill_rgba = day_style.bg, - stroke_width = mm_to_dots(day_style.frame_thickness)) - day = (day + 1) % 7 - - # draw month title (name) - mcolor = S.month.color_map_bg[year%2][month] - mcolor_fg = S.month.color_map_fg[year%2][month] - R_mb = grid.item(0) - draw_box(cr, rect = R_mb, stroke_rgba = S.month.frame, fill_rgba = mcolor, - stroke_width = mm_to_dots(S.month.frame_thickness)) # title box - draw_box(cr, rect = rect_from_origin(rect), stroke_rgba = S.month.frame, fill_rgba = (), - stroke_width = mm_to_dots(S.month.frame_thickness)) # full box - R_text = rect_rel_scale(R_mb, 1, 0.5) - mshad = None - if S.month.text_shadow: - f = S.month.text_shadow_size - mshad = (f,-f) if G.landscape else (f,f) - title_str = L.month_name[month] - if options.month_with_year: title_str += ' ' + str(year) - draw_str(cr, text = title_str, rect = R_text, stretch = -1, stroke_rgba = mcolor_fg, - align = (2,0), font = S.month.font, measure = mmeasure, shadow = mshad) - cr.restore() - - -#1 1 1 -#2 2 1 -#3 3 1 - -#4 2 2 -#5 3 2 -#6 3 2 -#7 4 2 -#8 4 2 - -#9 3 3 -#10 4 3 -#11 4 3 -#12 4 3 - -#rows = 0 -#cols = 0 - -def draw_calendar(Outfile, Year, Month, MonthSpan, Theme, holiday_provider, version_string): - S,G,L = Theme - rows, cols = options.rows, options.cols - - if options.symmetric: - G.month.symmetric = True - if options.padding is not None: - G.month.padding = options.padding - if options.no_shadow == True: - S.month.box_shadow = False - if Year % 2: options.swap_colors = not options.swap_colors - if options.swap_colors: - S.month.color_map_bg = (S.month.color_map_bg[1], S.month.color_map_bg[0]) - S.month.color_map_fg = (S.month.color_map_fg[1], S.month.color_map_fg[0]) - - try: - page = PageWriter(Outfile, G.landscape, G.pagespec, G.border, not options.opaque) - except InvalidFormat as e: - print >> sys.stderr, "invalid output format", e.args[0] - sys.exit(1) - - if rows == 0 and cols == 0: -# if MonthSpan < 4: -# cols = 1; rows = MonthSpan -# elif MonthSpan < 9: -# cols = 2; rows = int(math.ceil(MonthSpan/2.0)) -# else: - # TODO: improve this heuristic - cols = int(floor(sqrt(MonthSpan))) - rows = cols - if rows*cols < MonthSpan: rows += 1 - if rows*cols < MonthSpan: rows += 1 - if rows*cols < MonthSpan: cols += 1; rows -= 1 - if G.landscape: rows, cols = cols, rows - elif rows == 0: - rows = int(ceil(MonthSpan*1.0/cols)) - elif cols == 0: - cols = int(ceil(MonthSpan*1.0/rows)) - G.landscape = page.landscape # PNG is pseudo-landscape (portrait with width>height) - - if not options.no_footer: - V0 = VLayout(page.Text_rect, 40, (1,)*4) - Rcal = V0.item_span(39,0) - Rc = rect_rel_scale(V0.item(39),1,0.5,0,0) - else: - Rcal = page.Text_rect - - grid = GLayout(Rcal, rows, cols, pad = (mm_to_dots(G.month.padding),)*4) - mpp = grid.count() # months per page - num_pages = int(ceil(MonthSpan*1.0/mpp)) - cur_month = Month - cur_year = Year - num_placed = 0 - page_layout = [] - for k in xrange(num_pages): - page_layout.append([]) - for i in xrange(mpp): - page_layout[k].append((cur_month,cur_year)) - num_placed += 1 - cur_month += 1 - if cur_month > 12: cur_month = 1; cur_year += 1 - if num_placed >= MonthSpan: break + # draw box shadow + if S.month.box_shadow: + f = S.month.box_shadow_size + shad = (f,-f) if G.landscape else (f,f) + draw_shadow(cr, rect_from_origin(rect), shad) - num_pages_written = 0 - - z_order = options.z_order - if z_order == "auto": - if G.month.sloppy_dx != 0 or G.month.sloppy_dy != 0 or G.month.sloppy_rot != 0: - z_order = "decreasing" - else: - z_order = "increasing" - for p in page_layout: - num_placed = 0 - yy = [p[0][1]] - if z_order == "decreasing": p.reverse() - for (m,y) in p: - k = len(p) - num_placed - 1 if z_order == "decreasing" else num_placed - draw_month(page.cr, grid.item_seq(k, options.grid_order == "column"), - month=m, year=y, theme = Theme, holiday_provider = holiday_provider, - daycell_thres = options.short_daycell_ratio) - num_placed += 1 - if (y > yy[-1]): yy.append(y) - if not options.month_with_year and not options.no_footer: - year_str = str(yy[0]) if yy[0] == yy[-1] else "%s – %s" % (yy[0],yy[-1]) - draw_str(page.cr, text = year_str, rect = Rc, stroke_rgba = (0,0,0,0.5), stretch = -1, - align = (0,0), font = (extract_font_name(S.month.font),0,0)) - if not options.no_footer: - draw_str(page.cr, text = "rendered by Callirhoe ver. %s" % version_string, - rect = Rc, stroke_rgba = (0,0,0,0.5), stretch = -1, align = (1,0), - font = (extract_font_name(S.month.font),1,0)) - num_pages_written += 1 - page.end_page() - if num_pages_written < num_pages: - page.new_page() + # draw day cells + for dom in range(1,rows+1): + R = dom_grid.item(dom-1) + if dom <= span: + holiday_tuple = self.holiday_provider(year, month, dom, day) + day_style = holiday_tuple[2] + dcell = _base.DayCell(day = (dom, col), header = holiday_tuple[0], footer = holiday_tuple[1], + theme = (day_style, G.dom, L), show_day_name = True) + dcell.draw(cr, R, daycell_thres) + else: + day_style = S.dom + draw_box(cr, rect = R, stroke_rgba = day_style.frame, fill_rgba = day_style.bg, + stroke_width = mm_to_dots(day_style.frame_thickness)) + day = (day + 1) % 7 + + # draw month title (name) + mcolor = S.month.color_map_bg[year%2][month] + mcolor_fg = S.month.color_map_fg[year%2][month] + R_mb = grid.item(0) + draw_box(cr, rect = R_mb, stroke_rgba = S.month.frame, fill_rgba = mcolor, + stroke_width = mm_to_dots(S.month.frame_thickness)) # title box + draw_box(cr, rect = rect_from_origin(rect), stroke_rgba = S.month.frame, fill_rgba = (), + stroke_width = mm_to_dots(S.month.frame_thickness)) # full box + R_text = rect_rel_scale(R_mb, 1, 0.5) + mshad = None + if S.month.text_shadow: + f = S.month.text_shadow_size + mshad = (f,-f) if G.landscape else (f,f) + title_str = L.month_name[month] + if self.options.month_with_year: title_str += ' ' + str(year) + draw_str(cr, text = title_str, rect = R_text, stretch = -1, stroke_rgba = mcolor_fg, + align = (2,0), font = S.month.font, measure = mmeasure, shadow = mshad) + cr.restore() + diff --git a/layouts/classic.py b/layouts/classic.py index 4aa8c04..ab63c4e 100644 --- a/layouts/classic.py +++ b/layouts/classic.py @@ -19,21 +19,13 @@ from lib.xcairo import * from lib.geom import * -from math import floor, ceil, sqrt import calendar import sys from datetime import date, timedelta import _base -parser = _base.get_parser() - -options = None - -def setoptions(opt): - global options - options = opt - _base.options = opt +parser = _base.get_parser(__name__) def _weekrows_of_month(year, month): day,span = calendar.monthrange(year, month) @@ -42,183 +34,73 @@ def _weekrows_of_month(year, month): if day == 6 and span >= 30: return 6 return 5 -def draw_month(cr, rect, month, year, theme, holiday_provider, daycell_thres = 2.5): - S,G,L = theme - apply_rect(cr, rect, G.month.sloppy_dx, G.month.sloppy_dy, G.month.sloppy_rot) +class CalendarRenderer(_base.CalendarRenderer): + #default thres = 2.5 + def _draw_month(self, cr, rect, month, year, daycell_thres): + S,G,L = self.Theme + apply_rect(cr, rect, G.month.sloppy_dx, G.month.sloppy_dy, G.month.sloppy_rot) - day, span = calendar.monthrange(year, month) - weekrows = 6 if G.month.symmetric else _weekrows_of_month(year, month) - dom = -day + 1; - wmeasure = 'A'*max(map(len,L.day_name)) - mmeasure = 'A'*max(map(len,L.month_name)) - if options.month_with_year: - mmeasure += 'A'*(len(str(year))+1) + day, span = calendar.monthrange(year, month) + weekrows = 6 if G.month.symmetric else _weekrows_of_month(year, month) + dom = -day + 1; + wmeasure = 'A'*max(map(len,L.day_name)) + mmeasure = 'A'*max(map(len,L.month_name)) + if self.options.month_with_year: + mmeasure += 'A'*(len(str(year))+1) - grid = GLayout(rect_from_origin(rect), 7, 7) - # 61.8% - 38.2% split (golden) - R_mb, R_db = rect_vsplit(grid.item_span(1, 7, 0, 0), 0.618) # month name bar, day name bar - R_dnc = HLayout(R_db, 7) # day name cells = 1/7-th of day name bar - dom_grid = GLayout(grid.item_span(6, 7, 1, 0), weekrows, 7) - - # draw box shadow - if S.month.box_shadow: - f = S.month.box_shadow_size - shad = (f,-f) if G.landscape else (f,f) - draw_shadow(cr, rect_from_origin(rect), shad) + grid = GLayout(rect_from_origin(rect), 7, 7) + # 61.8% - 38.2% split (golden) + R_mb, R_db = rect_vsplit(grid.item_span(1, 7, 0, 0), 0.618) # month name bar, day name bar + R_dnc = HLayout(R_db, 7) # day name cells = 1/7-th of day name bar + dom_grid = GLayout(grid.item_span(6, 7, 1, 0), weekrows, 7) - # draw day names - for col in range(7): - R = R_dnc.item(col) - draw_box(cr, rect = R, stroke_rgba = S.dom.frame, - fill_rgba = S.dom.bg if col < 5 else S.dom_weekend.bg, - stroke_width = mm_to_dots(S.dow.frame_thickness)) - R_text = rect_rel_scale(R, 1, 0.5) - draw_str(cr, text = L.day_name[col], rect = R_text, stretch = -1, stroke_rgba = S.dow.fg, - align = (2,0), font = S.dow.font, measure = wmeasure) - - # draw day cells - for row in range(weekrows): + # draw box shadow + if S.month.box_shadow: + f = S.month.box_shadow_size + shad = (f,-f) if G.landscape else (f,f) + draw_shadow(cr, rect_from_origin(rect), shad) + + # draw day names for col in range(7): - R = dom_grid.item(row, col) - if dom > 0 and dom <= span: - holiday_tuple = holiday_provider(year, month, dom, col) - day_style = holiday_tuple[2] - dcell = _base.DayCell(day = (dom, col), header = holiday_tuple[0], footer = holiday_tuple[1], - theme = (day_style, G.dom, L), show_day_name = False) - dcell.draw(cr, R, daycell_thres) - else: - day_style = S.dom_weekend if col >= 5 else S.dom - draw_box(cr, rect = R, stroke_rgba = day_style.frame, fill_rgba = day_style.bg, - stroke_width = mm_to_dots(day_style.frame_thickness)) - dom += 1 + R = R_dnc.item(col) + draw_box(cr, rect = R, stroke_rgba = S.dom.frame, + fill_rgba = S.dom.bg if col < 5 else S.dom_weekend.bg, + stroke_width = mm_to_dots(S.dow.frame_thickness)) + R_text = rect_rel_scale(R, 1, 0.5) + draw_str(cr, text = L.day_name[col], rect = R_text, stretch = -1, stroke_rgba = S.dow.fg, + align = (2,0), font = S.dow.font, measure = wmeasure) - # draw month title (name) - mcolor = S.month.color_map_bg[year%2][month] - mcolor_fg = S.month.color_map_fg[year%2][month] - draw_box(cr, rect = R_mb, stroke_rgba = S.month.frame, fill_rgba = mcolor, - stroke_width = mm_to_dots(S.month.frame_thickness)) # title box - draw_box(cr, rect = rect_from_origin(rect), stroke_rgba = S.month.frame, fill_rgba = (), - stroke_width = mm_to_dots(S.month.frame_thickness)) # full box - R_text = rect_rel_scale(R_mb, 1, 0.5) - mshad = None - if S.month.text_shadow: - f = S.month.text_shadow_size - mshad = (f,-f) if G.landscape else (f,f) - title_str = L.month_name[month] - if options.month_with_year: title_str += ' ' + str(year) - draw_str(cr, text = title_str, rect = R_text, stretch = -1, stroke_rgba = mcolor_fg, - align = (2,0), font = S.month.font, measure = mmeasure, shadow = mshad) - cr.restore() + # draw day cells + for row in range(weekrows): + for col in range(7): + R = dom_grid.item(row, col) + if dom > 0 and dom <= span: + holiday_tuple = self.holiday_provider(year, month, dom, col) + day_style = holiday_tuple[2] + dcell = _base.DayCell(day = (dom, col), header = holiday_tuple[0], footer = holiday_tuple[1], + theme = (day_style, G.dom, L), show_day_name = False) + dcell.draw(cr, R, daycell_thres) + else: + day_style = S.dom_weekend if col >= 5 else S.dom + draw_box(cr, rect = R, stroke_rgba = day_style.frame, fill_rgba = day_style.bg, + stroke_width = mm_to_dots(day_style.frame_thickness)) + dom += 1 + + # draw month title (name) + mcolor = S.month.color_map_bg[year%2][month] + mcolor_fg = S.month.color_map_fg[year%2][month] + draw_box(cr, rect = R_mb, stroke_rgba = S.month.frame, fill_rgba = mcolor, + stroke_width = mm_to_dots(S.month.frame_thickness)) # title box + draw_box(cr, rect = rect_from_origin(rect), stroke_rgba = S.month.frame, fill_rgba = (), + stroke_width = mm_to_dots(S.month.frame_thickness)) # full box + R_text = rect_rel_scale(R_mb, 1, 0.5) + mshad = None + if S.month.text_shadow: + f = S.month.text_shadow_size + mshad = (f,-f) if G.landscape else (f,f) + title_str = L.month_name[month] + if self.options.month_with_year: title_str += ' ' + str(year) + draw_str(cr, text = title_str, rect = R_text, stretch = -1, stroke_rgba = mcolor_fg, + align = (2,0), font = S.month.font, measure = mmeasure, shadow = mshad) + cr.restore() -#1 1 1 -#2 2 1 -#3 3 1 - -#4 2 2 -#5 3 2 -#6 3 2 -#7 4 2 -#8 4 2 - -#9 3 3 -#10 4 3 -#11 4 3 -#12 4 3 - -#rows = 0 -#cols = 0 - -def draw_calendar(Outfile, Year, Month, MonthSpan, Theme, holiday_provider, version_string): - S,G,L = Theme - rows, cols = options.rows, options.cols - - if options.symmetric: - G.month.symmetric = True - if options.padding is not None: - G.month.padding = options.padding - if options.no_shadow == True: - S.month.box_shadow = False - if Year % 2: options.swap_colors = not options.swap_colors - if options.swap_colors: - S.month.color_map_bg = (S.month.color_map_bg[1], S.month.color_map_bg[0]) - S.month.color_map_fg = (S.month.color_map_fg[1], S.month.color_map_fg[0]) - - try: - page = PageWriter(Outfile, G.landscape, G.pagespec, G.border, not options.opaque) - except InvalidFormat as e: - print >> sys.stderr, "invalid output format", e.args[0] - sys.exit(1) - - if rows == 0 and cols == 0: -# if MonthSpan < 4: -# cols = 1; rows = MonthSpan -# elif MonthSpan < 9: -# cols = 2; rows = int(math.ceil(MonthSpan/2.0)) -# else: - # TODO: improve this heuristic - cols = int(floor(sqrt(MonthSpan))) - rows = cols - if rows*cols < MonthSpan: rows += 1 - if rows*cols < MonthSpan: rows += 1 - if rows*cols < MonthSpan: cols += 1; rows -= 1 - if G.landscape: rows, cols = cols, rows - elif rows == 0: - rows = int(ceil(MonthSpan*1.0/cols)) - elif cols == 0: - cols = int(ceil(MonthSpan*1.0/rows)) - G.landscape = page.landscape # PNG is pseudo-landscape (portrait with width>height) - - if not options.no_footer: - V0 = VLayout(page.Text_rect, 40, (1,)*4) - Rcal = V0.item_span(39,0) - Rc = rect_rel_scale(V0.item(39),1,0.5,0,0) - else: - Rcal = page.Text_rect - - grid = GLayout(Rcal, rows, cols, pad = (mm_to_dots(G.month.padding),)*4) - mpp = grid.count() # months per page - num_pages = int(ceil(MonthSpan*1.0/mpp)) - cur_month = Month - cur_year = Year - num_placed = 0 - page_layout = [] - for k in xrange(num_pages): - page_layout.append([]) - for i in xrange(mpp): - page_layout[k].append((cur_month,cur_year)) - num_placed += 1 - cur_month += 1 - if cur_month > 12: cur_month = 1; cur_year += 1 - if num_placed >= MonthSpan: break - - num_pages_written = 0 - - z_order = options.z_order - if z_order == "auto": - if G.month.sloppy_dx != 0 or G.month.sloppy_dy != 0 or G.month.sloppy_rot != 0: - z_order = "decreasing" - else: - z_order = "increasing" - for p in page_layout: - num_placed = 0 - yy = [p[0][1]] - if z_order == "decreasing": p.reverse() - for (m,y) in p: - k = len(p) - num_placed - 1 if z_order == "decreasing" else num_placed - draw_month(page.cr, grid.item_seq(k, options.grid_order == "column"), - month=m, year=y, theme = Theme, holiday_provider = holiday_provider, - daycell_thres = options.short_daycell_ratio) - num_placed += 1 - if (y > yy[-1]): yy.append(y) - if not options.month_with_year and not options.no_footer: - year_str = str(yy[0]) if yy[0] == yy[-1] else "%s – %s" % (yy[0],yy[-1]) - draw_str(page.cr, text = year_str, rect = Rc, stroke_rgba = (0,0,0,0.5), stretch = -1, - align = (0,0), font = (extract_font_name(S.month.font),0,0)) - if not options.no_footer: - draw_str(page.cr, text = "rendered by Callirhoe ver. %s" % version_string, - rect = Rc, stroke_rgba = (0,0,0,0.5), stretch = -1, align = (1,0), - font = (extract_font_name(S.month.font),1,0)) - num_pages_written += 1 - page.end_page() - if num_pages_written < num_pages: - page.new_page() From 11e2ac03130dc994701fb9e3684a35f0c96f0541 Mon Sep 17 00:00:00 2001 From: "geortz@gmail.com" Date: Thu, 28 Aug 2014 16:31:41 +0000 Subject: [PATCH 13/17] restructured code in main script epydoc strings git-svn-id: https://callirhoe.googlecode.com/svn/branches/next@45 81c8bb96-aa45-f2e2-0eef-c4fa4a15c6df --- callirhoe.py | 369 ++++++++++++++++++++++++++--------------------- layouts/_base.py | 4 - lib/geom.py | 89 +++++++++++- lib/holiday.py | 58 ++++---- lib/plugin.py | 19 ++- lib/xcairo.py | 11 +- 6 files changed, 340 insertions(+), 210 deletions(-) diff --git a/callirhoe.py b/callirhoe.py index 32938c5..48024c8 100755 --- a/callirhoe.py +++ b/callirhoe.py @@ -17,11 +17,13 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see http://www.gnu.org/licenses/ +"""high quality calendar rendering""" + # TODO: -# separate bar/matrix [WIP] - move common code aside (base class?) +# default values for rows/cols depending on layout (classic/bars) # fix auto-measure rendering (cairo) - +# fix plugin loading (without global vars) # allow to change background color (fill), other than white # page spec parse errors @@ -53,9 +55,24 @@ import lib.holiday as holiday from lib.plugin import * # TODO: SEE IF IT CAN BE MOVED INTO lib.plugin ... -def import_plugin(cat, longcat, longcat2, listopt, preset): +def import_plugin(plugin_paths, cat, longcat, longcat2, listopt, preset): + """import a plugin making it visible + + I{Example:} + + >>> Language = import_plugin(get_plugin_paths(), "lang", "language", "languages", "--list-languages", "EN") + + @param plugin_paths: list of plugin search paths + @param cat: short category name (for folder name) + @param longcat: long category name + @param longcat2: long category name in plural form + @param listopt: option name + @param preset: default value + + @note: Aimed for internal use with I{lang}, I{style}, I{geom}, I{layouts}. + """ try: - found = available_files(plugin_path[0], cat, preset) + available_files(plugin_path[1], cat, preset) + found = available_files(plugin_paths[0], cat, preset) + available_files(plugin_paths[1], cat, preset) if len(found) == 0: raise IOError old = sys.path[0]; sys.path[0] = found[0][1] @@ -70,38 +87,8 @@ def import_plugin(cat, longcat, longcat2, listopt, preset): print >> sys.stderr, "error loading %s definition `%s'" % (longcat, preset) sys.exit(1) -parser = optparse.OptionParser(usage="usage: %prog [options] [[MONTH[-MONTH2|:SPAN]] YEAR] FILE", - description="""High quality calendar rendering with vector graphics. -By default, a calendar of the current year in pdf format is written to FILE. -Alternatively, you can select a specific YEAR (0=current), -and a month range from MONTH (0-12, 0=current) to MONTH2 or for SPAN months. -""", version="callirhoe " + _version) -parser.add_option("-l", "--lang", dest="lang", default="EN", - help="choose language [%default]") -parser.add_option("-t", "--layout", dest="layout", default="classic", - help="choose layout [%default]") -parser.add_option("-?", "--layout-help", dest="layouthelp", action="store_true", default=False, - help="show layout-specific help") -parser.add_option("--examples", dest="examples", action="store_true", - help="display some usage examples") -parser.add_option("-s", "--style", dest="style", default="default", - help="choose style [%default]") -parser.add_option("-g", "--geometry", dest="geom", default="default", - help="choose geometry [%default]") -parser.add_option("--landscape", action="store_true", dest="landscape", default=False, - help="landscape mode") -parser.add_option("--dpi", type="float", default=72.0, - help="set DPI (for raster output) [%default]") -parser.add_option("--paper", default="a4", - help="set paper type; PAPER can be an ISO paper type (a0..a9 or a0w..a9w) or of the " - "form W:H; positive values correspond to W or H mm, negative values correspond to " - "-W or -H pixels; 'w' suffix swaps width & height [%default]") -parser.add_option("--border", type="float", default=3, - help="set border size (in mm) [%default]") -parser.add_option("-H", "--with-holidays", action="append", dest="holidays", - help="load holiday file (can be used multiple times)") - def print_examples(): + """print usage examples""" print """Examples: Create a calendar of the current year (by default in a 4x3 grid): @@ -130,89 +117,15 @@ and do some magic with ImageMagick! ;) """ def add_list_option(parser, opt): + """add a --list-I{plugins} option to parser + + @note: To be used with I{languages}, I{layouts}, I{styles} and I{geometries}. + """ parser.add_option("--list-%s" % opt, action="store_true", dest="list_%s" % opt, default=False, help="list available %s" % opt) -for x in ["languages", "layouts", "styles", "geometries"]: - add_list_option(parser, x) - -parser.add_option("--lang-var", action="append", dest="lang_assign", - help="modify a language variable") -parser.add_option("--style-var", action="append", dest="style_assign", - help="modify a style variable, e.g. dom.frame_thickness=0") -parser.add_option("--geom-var", action="append", dest="geom_assign", - help="modify a geometry variable") - -argv1 = [] -argv2 = [] -for x in sys.argv: - y = x[0:x.find('=')] if '=' in x else x - if x[0] == '-' and not parser.has_option(y): - argv2.append(x) - else: - argv1.append(x) -sys.argv = argv1 - -(options,args) = parser.parse_args() - -if options.list_languages: - for x in plugin_list("lang"): print x[0], - print -if options.list_styles: - for x in plugin_list("style"): print x[0], - print -if options.list_geometries: - for x in plugin_list("geom"): print x[0], - print -if options.list_layouts: - for x in plugin_list("layouts"): print x[0], - print -if (options.list_languages or options.list_styles or - options.list_geometries or options.list_layouts): sys.exit(0) - -Language = import_plugin("lang", "language", "languages", "--list-languages", options.lang) -Style = import_plugin("style", "style", "styles", "--list-styles", options.style) -Geometry = import_plugin("geom", "geometry", "geometries", "--list-geometries", options.geom) -Layout = import_plugin("layouts", "layout", "layouts", "--list-layouts", options.layout) - -for x in argv2: - if '=' in x: x = x[0:x.find('=')] - if not Layout.parser.has_option(x): - parser.error("invalid option %s; use --help (-h) or --layout-help (-?) to see available options" % x) - -(loptions,largs) = Layout.parser.parse_args(argv2) - -if options.layouthelp: - #print "Help for layout:", options.layout - Layout.parser.print_help() - sys.exit(0) - -if options.examples: - print_examples() - sys.exit(0) - -# we can put it separately together with Layout; but we load Layout *after* lang,style,geom -if len(args) < 1 or len(args) > 3: - parser.print_help() - sys.exit(0) - -#if (len(args[-1]) == 4 and args[-1].isdigit()): -# print "WARNING: file name `%s' looks like a year, writing anyway..." % args[-1] - -# the usual "beware of exec()" crap applies here... but come on, -# this is a SCRIPTING language, you can always hack the source code!!! -if options.lang_assign: - for x in options.lang_assign: exec "Language.%s" % x -if options.style_assign: - for x in options.style_assign: exec "Style.%s" % x -if options.geom_assign: - for x in options.geom_assign: exec "Geometry.%s" % x - -calendar.long_month_name = Language.long_month_name -calendar.long_day_name = Language.long_day_name -calendar.short_month_name = Language.short_month_name -calendar.short_day_name = Language.short_day_name def itoa(s): + """convert integer to string, exiting on error (for cmdline parsing)""" try: k = int(s); except ValueError as e: @@ -220,66 +133,190 @@ def itoa(s): return k def parse_month(mstr): + """get a month value (0-12) from I{mstr}, exiting on error (for cmdline parsing)""" m = itoa(mstr) - if m < 1: m = time.localtime()[1] - elif m > 12: sys.exit("invalid month value `" + str(mstr) + "'") + if m == 0: m = time.localtime()[1] + elif m > 12 or m < 0: sys.exit("invalid month value `" + str(mstr) + "'") return m def parse_year(ystr): + """get a year value (>=0) from I{ystr}, exiting on error (for cmdline parsing)""" y = itoa(ystr) if y == 0: y = time.localtime()[0] + elif y < 0: sys.exit("invalid year value `" + str(ystr) + "'") return y -if len(args) == 1: - Year = time.localtime()[0] - Month, MonthSpan = 1, 12 - Outfile = args[0] -elif len(args) == 2: - Year = parse_year(args[0]) - Month, MonthSpan = 1, 12 - Outfile = args[1] -elif len(args) == 3: - if ':' in args[0]: - t = args[0].split(':') - if len(t) != 2: sys.exit("invalid month range `" + args[0] + "'") - Month = parse_month(t[0]) - MonthSpan = itoa(t[1]) - if MonthSpan < 0: sys.exit("invalid month range `" + args[0] + "'") - elif '-' in args[0]: - t = args[0].split('-') - if len(t) != 2: sys.exit("invalid month range `" + args[0] + "'") - Month = parse_month(t[0]) - MonthSpan = itoa(t[1]) - Month + 1 - if MonthSpan < 0: sys.exit("invalid month range `" + args[0] + "'") +if __name__ == "__main__": + parser = optparse.OptionParser(usage="usage: %prog [options] [[MONTH[-MONTH2|:SPAN]] YEAR] FILE", + description="""High quality calendar rendering with vector graphics. + By default, a calendar of the current year in pdf format is written to FILE. + Alternatively, you can select a specific YEAR (0=current), + and a month range from MONTH (0-12, 0=current) to MONTH2 or for SPAN months. + """, version="callirhoe " + _version) + parser.add_option("-l", "--lang", dest="lang", default="EN", + help="choose language [%default]") + parser.add_option("-t", "--layout", dest="layout", default="classic", + help="choose layout [%default]") + parser.add_option("-?", "--layout-help", dest="layouthelp", action="store_true", default=False, + help="show layout-specific help") + parser.add_option("--examples", dest="examples", action="store_true", + help="display some usage examples") + parser.add_option("-s", "--style", dest="style", default="default", + help="choose style [%default]") + parser.add_option("-g", "--geometry", dest="geom", default="default", + help="choose geometry [%default]") + parser.add_option("--landscape", action="store_true", dest="landscape", default=False, + help="landscape mode") + parser.add_option("--dpi", type="float", default=72.0, + help="set DPI (for raster output) [%default]") + parser.add_option("--paper", default="a4", + help="set paper type; PAPER can be an ISO paper type (a0..a9 or a0w..a9w) or of the " + "form W:H; positive values correspond to W or H mm, negative values correspond to " + "-W or -H pixels; 'w' suffix swaps width & height [%default]") + parser.add_option("--border", type="float", default=3, + help="set border size (in mm) [%default]") + parser.add_option("-H", "--with-holidays", action="append", dest="holidays", + help="load holiday file (can be used multiple times)") + parser.add_option("--short-monthnames", action="store_true", default=False, + help="user the short version of month names (defined in language file) [%default]") + parser.add_option("--long-daynames", action="store_true", default=False, + help="user the long version of day names (defined in language file) [%default]") + + for x in ["languages", "layouts", "styles", "geometries"]: + add_list_option(parser, x) + + parser.add_option("--lang-var", action="append", dest="lang_assign", + help="modify a language variable") + parser.add_option("--style-var", action="append", dest="style_assign", + help="modify a style variable, e.g. dom.frame_thickness=0") + parser.add_option("--geom-var", action="append", dest="geom_assign", + help="modify a geometry variable") + + argv1 = [] + argv2 = [] + for x in sys.argv: + y = x[0:x.find('=')] if '=' in x else x + if x[0] == '-' and not parser.has_option(y): + argv2.append(x) + else: + argv1.append(x) + sys.argv = argv1 + + (options,args) = parser.parse_args() + + list_and_exit = False + if options.list_languages: + for x in plugin_list("lang"): print x[0], + print + list_and_exit = True + if options.list_styles: + for x in plugin_list("style"): print x[0], + print + list_and_exit = True + if options.list_geometries: + for x in plugin_list("geom"): print x[0], + print + list_and_exit = True + if options.list_layouts: + for x in plugin_list("layouts"): print x[0], + print + list_and_exit = True + if list_and_exit: sys.exit(0) + + plugin_paths = get_plugin_paths() + Language = import_plugin(plugin_paths, "lang", "language", "languages", "--list-languages", options.lang) + Style = import_plugin(plugin_paths, "style", "style", "styles", "--list-styles", options.style) + Geometry = import_plugin(plugin_paths, "geom", "geometry", "geometries", "--list-geometries", options.geom) + Layout = import_plugin(plugin_paths, "layouts", "layout", "layouts", "--list-layouts", options.layout) + + for x in argv2: + if '=' in x: x = x[0:x.find('=')] + if not Layout.parser.has_option(x): + parser.error("invalid option %s; use --help (-h) or --layout-help (-?) to see available options" % x) + + (loptions,largs) = Layout.parser.parse_args(argv2) + + if options.layouthelp: + #print "Help for layout:", options.layout + Layout.parser.print_help() + sys.exit(0) + + if options.examples: + print_examples() + sys.exit(0) + + # we can put it separately together with Layout; but we load Layout *after* lang,style,geom + if len(args) < 1 or len(args) > 3: + parser.print_help() + sys.exit(0) + + #if (len(args[-1]) == 4 and args[-1].isdigit()): + # print "WARNING: file name `%s' looks like a year, writing anyway..." % args[-1] + + # the usual "beware of exec()" crap applies here... but come on, + # this is a SCRIPTING language, you can always hack the source code!!! + if options.lang_assign: + for x in options.lang_assign: exec "Language.%s" % x + if options.style_assign: + for x in options.style_assign: exec "Style.%s" % x + if options.geom_assign: + for x in options.geom_assign: exec "Geometry.%s" % x + + calendar.long_month_name = Language.long_month_name + calendar.long_day_name = Language.long_day_name + calendar.short_month_name = Language.short_month_name + calendar.short_day_name = Language.short_day_name + + if len(args) == 1: + Year = time.localtime()[0] + Month, MonthSpan = 1, 12 + Outfile = args[0] + elif len(args) == 2: + Year = parse_year(args[0]) + Month, MonthSpan = 1, 12 + Outfile = args[1] + elif len(args) == 3: + if ':' in args[0]: + t = args[0].split(':') + if len(t) != 2: sys.exit("invalid month range `" + args[0] + "'") + Month = parse_month(t[0]) + MonthSpan = itoa(t[1]) + if MonthSpan < 0: sys.exit("invalid month range `" + args[0] + "'") + elif '-' in args[0]: + t = args[0].split('-') + if len(t) != 2: sys.exit("invalid month range `" + args[0] + "'") + Month = parse_month(t[0]) + MonthSpan = itoa(t[1]) - Month + 1 + if MonthSpan < 0: sys.exit("invalid month range `" + args[0] + "'") + else: + Month = parse_month(args[0]) + MonthSpan = 1 + Year = parse_year(args[1]) + Outfile = args[2] + + Geometry.landscape = options.landscape + xcairo.XDPI = options.dpi + Geometry.pagespec = options.paper + Geometry.border = options.border + + hprovider = holiday.HolidayProvider(Style.dom, Style.dom_weekend, + Style.dom_holiday, Style.dom_weekend_holiday, + Style.dom_multi, Style.dom_weekend_multi) + + if options.holidays: + for f in options.holidays: + hprovider.load_holiday_file(f) + + if options.long_daynames: + Language.day_name = Language.long_day_name else: - Month = parse_month(args[0]) - MonthSpan = 1 - Year = parse_year(args[1]) - Outfile = args[2] + Language.day_name = Language.short_day_name -Geometry.landscape = options.landscape -xcairo.XDPI = options.dpi -Geometry.pagespec = options.paper -Geometry.border = options.border + if options.short_monthnames: + Language.month_name = Language.short_month_name + else: + Language.month_name = Language.long_month_name -hprovider = holiday.HolidayProvider(Style.dom, Style.dom_weekend, - Style.dom_holiday, Style.dom_weekend_holiday, - Style.dom_multi, Style.dom_weekend_multi) - -if options.holidays: - for f in options.holidays: - hprovider.load_holiday_file(f) - -if loptions.long_daynames: - Language.day_name = Language.long_day_name -else: - Language.day_name = Language.short_day_name - -if loptions.short_monthnames: - Language.month_name = Language.short_month_name -else: - Language.month_name = Language.long_month_name - -renderer = Layout.CalendarRenderer(Outfile, Year, Month, MonthSpan, - (Style,Geometry,Language), hprovider, _version, loptions) -renderer.render() + renderer = Layout.CalendarRenderer(Outfile, Year, Month, MonthSpan, + (Style,Geometry,Language), hprovider, _version, loptions) + renderer.render() diff --git a/layouts/_base.py b/layouts/_base.py index ee726b1..b18646e 100644 --- a/layouts/_base.py +++ b/layouts/_base.py @@ -39,10 +39,6 @@ def get_parser(layout_name): "selects increasing order if and only if sloppy boxes are enabled [%default]") parser.add_option("--month-with-year", action="store_true", default=False, help="displays year together with month name, e.g. January 1980; suppresses year from footer line") - parser.add_option("--short-monthnames", action="store_true", default=False, - help="user the short version of month names (defined in language file) [%default]") - parser.add_option("--long-daynames", action="store_true", default=False, - help="user the long version of day names (defined in language file) [%default]") parser.add_option("--long-daycells", action="store_const", const=0.0, dest="short_daycell_ratio", help="force use of only long daycells") parser.add_option("--short-daycells", action="store_const", const=1.0e6, dest="short_daycell_ratio", diff --git a/lib/geom.py b/lib/geom.py index cf0bc03..beea089 100644 --- a/lib/geom.py +++ b/lib/geom.py @@ -18,40 +18,68 @@ # ***************************************** # # -# general-purpose geometry routines # +""" general-purpose geometry routines """ # # # ***************************************** def rect_ratio(r): + """returns the ratio of rect I{r} which is defined as M{width/height}""" return r[2]*1.0/r[3] def rect_rel_scale(r, fw, fh, align_x = 0, align_y = 0): + """relatively scale a rect + + @type fw: float in [0,1] + @param fw: width fraction (to be multiplied) + @type fh: float in [0,1] + @param fh: height fraction (to be multiplied) + @type align_x: float in [-1,1] + @param align_x: determines the relative position of the new rect with respect to + the old one. A value of 0 aligns in the center, a value of -1 aligns on the + left, a value of 1 aligns on the right hand side. Intermediate values do + linear interpolation. + @type align_y: float in [-1,1] + @param align_y: Performs vertical (top-bottom) alignment similarly to L{align_x}. + """ x, y, w, h = r return (x + (align_x + 1.0)*w*(1 - fw)/2.0, y + (align_y + 1.0)*h*(1 - fh)/2.0, w*fw, h*fh) def rect_pad(r, pad): + """returns a padded rect by reducing border by the I{pad} tuple (top,left,bottom,right)""" x, y, w, h = r t_, l_, b_, r_ = pad return (x + l_, y + t_, w - r_ - l_, h - t_ - b_) def rect_to_abs(r): + """get absolute coordinates (x0,y0,x1,y1) from rect definition (x,y,w,h)""" x, y, w, h = r return (x, y, x + w, y + h) def abs_to_rect(a): + """get rect definition (x,y,w,h) from absolute coordinates (x0,y0,x1,y1)""" x1, y1, x2, y2 = a return (x1, y1, x2 - x1, y2 - y1) def rect_from_origin(r): + """returns a similar rect with top-left corner at (0,0)""" return (0, 0, r[2], r[3]) def rect_hull(r1,r2): + """returns the smallest rect containing r1 and r2""" x1, y1, x2, y2 = rect_to_abs(r1) x3, y3, x4, y4 = rect_to_abs(r2) return abs_to_rect((min(x1,x3), min(y1,y3), max(x2,x4), max(y2,y4))) def rect_hsplit(r, f = 0.5, fdist = 0.0): + """split a rect horizontally + + @type f: float in [0,1] + @param f: split fraction + @param fdist: fraction of space to discard before splitting (free space) + @return: tuple (r1,r2) with splits and free space evenly distributed + before r1, between r1 and r2 and after r2 + """ x, y, w, h = r rw = w*(1.0 - fdist) r1 = (x + w*fdist/3.0, y, rw*f, h) @@ -59,6 +87,7 @@ def rect_hsplit(r, f = 0.5, fdist = 0.0): return (r1, r2) def rect_vsplit(r, f = 0.5, fdist = 0.0): + """split a rect vertically, similarly to L{rect_hsplit}""" x, y, w, h = r rh = h*(1.0 - fdist) r1 = (x, y + h*fdist/3.0, w, rh*f) @@ -66,45 +95,73 @@ def rect_vsplit(r, f = 0.5, fdist = 0.0): return (r1, r2) def color_mix(a, b, frac): + """mix two colors + + @type frac: float in [0,1] + @param frac: amount of first color + """ return map(lambda (x,y): x*frac + y*(1 - frac), zip(a,b)) def color_scale(a, frac): + """scale color values + + @type frac: float + @param frac: scale amount (to be multiplied) + """ return map(lambda x: min(1.0,x*frac), a) def color_auto_fg(bg, light = (1,1,1), dark = (0,0,0)): + """return I{light} or I{dark} foreground color based on an ad-hoc evaluation of I{bg}""" return light if (bg[0] + 1.5*bg[1] + bg[2]) < 1.0 else dark # ********* layout managers *********** class VLayout(object): + """vertical layout manager + + @ivar rect: bounding rect for layout -- this rect will be split and the slices assigned to every item + @ivar nitems: maximum number of items in the layout + @ivar pad: tuple(top,left,bottom,right) with item padding + """ def __init__(self, rect, nitems = 1, pad = (0.0,0.0,0.0,0.0)): # TLBR self.rect = rect self.nitems = nitems self.pad = pad def count(self): + """return maximum number of items in the layout""" return self.nitems def resize(self, k): + """set maximum number of items""" self.nitems = k def grow(self, delta = 1): + """increase number of items by I{delta}""" self.nitems += delta def item(self, i = 0): + """get rect for item I{i}""" x, y, w, h = self.rect h *= 1.0/self.nitems y += i*h return rect_pad((x,y,w,h), self.pad) def item_span(self, n, k = -1): + """get union of I{k} consecutive items, starting at position I{n} + + @param n: first item + @param k: number of items, -1 for all remaining items + """ if k < 0: k = (self.count() - n) // 2 return rect_hull(self.item(k), self.item(k + n - 1)) def items(self): + """returns a sequence of all items""" return map(self.item, range(self.count())) -class HLayout(VLayout): # transpose of VLayout +class HLayout(VLayout): + """horizontal layout manager defined as a transpose of L{VLayout}""" def __init__(self, rect, nitems = 1, pad = (0.0,0.0,0.0,0.0)): # TLBR super(HLayout,self).__init__((rect[1],rect[0],rect[3],rect[2]), nitems, (pad[1], pad[0], pad[3], pad[2])) @@ -114,31 +171,49 @@ class HLayout(VLayout): # transpose of VLayout return (t[1], t[0], t[3], t[2]) class GLayout: + """grid layout manager + + @ivar vrep: internal L{VLayout} for row computations + @ivar hrep: internal L{HLayout} for column computations + """ def __init__(self, rect, nrows = 1, ncols = 1, pad = (0.0,0.0,0.0,0.0)): # TLBR + """initialize layout + + @param rect: layout rect (tuple) + @param nrows: number of rows + @param ncols: number of columns + @param pad: cell padding + """ self.vrep = VLayout(rect, nrows, (pad[0], 0.0, pad[2], 0.0)) t = self.vrep.item(0) self.hrep = HLayout((rect[0], rect[1], t[2], t[3]), ncols, (0.0, pad[1], 0.0, pad[3])) def row_count(self): + """get (max) number of rows in the grid""" return self.vrep.count() def col_count(self): + """get (max) number of columns in the grid""" return self.hrep.count() def count(self): + """get total number of cells in the grid (which is M{rows*cols})""" return self.row_count()*self.col_count() def resize(self, rows, cols): + """resize grid by specifying new number of rows and columns""" self.vrep.resize(rows) t = self.vrep.item(0) self.hrep = HLayout(t[0:2], t[2:4], cols, (0.0, pad[1], 0.0, pad[3])) def item(self, row, col): + """get rect of cell at position I{row,col}""" ty = self.vrep.item(row) tx = self.hrep.item(col) return (tx[0], ty[1], tx[2], tx[3]) def item_seq(self, k, column_wise = False): + """get rect of cell at position I{k} column-wise or row-wise""" if not column_wise: row, col = k // self.col_count(), k % self.col_count() else: @@ -146,16 +221,26 @@ class GLayout: return self.item(row, col) def items(self, column_wise = False): + """get sequence of rects of cells column-wise or row-wise""" return map(self.item_seq, range(self.count())) def row_items(self, row): + """get sequence of cell rects of a row""" return map(lambda x: self.item(row, x), range(self.col_count())) def col_items(self, col): + """get sequence of cell rects of a column""" return map(lambda x: self.item(x, col), range(self.row_count())) def item_span(self, nr, nc, row = -1, col = -1): + """get union of cell rects spanning a subgrid + + @param nr: number of spanning rows + @param nc: number of spanning columns + @param row: starting row, -1 for vertically centered + @param col: starting column, -1 for horizontally centered + """ if row < 0: row = (self.row_count() - nr) // 2 if col < 0: col = (self.col_count() - nc) // 2 return rect_hull(self.item(row, col), self.item(row + nr - 1, col + nc - 1)) diff --git a/lib/holiday.py b/lib/holiday.py index b1c833f..ac0b8f1 100644 --- a/lib/holiday.py +++ b/lib/holiday.py @@ -18,14 +18,14 @@ # ***************************************** # # -# holiday support routines # +""" holiday support routines """ # # # ***************************************** from datetime import date, timedelta def _get_orthodox_easter(year): - """compute date of orthodox easter""" + """Compute date of orthodox easter.""" y1, y2, y3 = year % 4 , year % 7, year % 19 a = 19*y3 + 15 y4 = a % 30 @@ -37,7 +37,7 @@ def _get_orthodox_easter(year): # return res def _get_catholic_easter(year): - """compute date of catholic easter""" + """Compute date of catholic easter.""" a, b, c = year % 19, year // 100, year % 100 d, e = divmod(b,4) f = (b + 8) // 25 @@ -50,22 +50,24 @@ def _get_catholic_easter(year): return date(year, emonth, edate+1) class Holiday(object): - """class holding a Holiday object (date is not stored!) + """class holding a Holiday object (date is I{not} stored!) - Properties: - header: string for header - footer: string for footer - flags : bit combination of {OFF=1, MULTI=2, REMINDER=4} - OFF: day off (real holiday) - MULTI: multi-day event (used to mark long day ranges, - not necessarily holidays) - REMINDER: do not mark the day as holiday + @ivar header: string for header (primary text) + @ivar footer: string for footer (secondary text) + @ivar flags: bit combination of {OFF=1, MULTI=2, REMINDER=4} - Remarks: - Rendering style is considered in the following order: - 1) OFF - 2) MULTI - First flag that matches determines the style + I{OFF}: day off (real holiday) + + I{MULTI}: multi-day event (used to mark long day ranges, + not necessarily holidays) + + I{REMINDER}: do not mark the day as holiday + + @note: Rendering style is determined considering L{flags} in this order: + 1. OFF + 2. MULTI + + First flag that matches determines the style. """ OFF = 1 MULTI = 2 @@ -76,6 +78,7 @@ class Holiday(object): self.flags = self._parse_flags(flags_str) def merge_with(self, hol_list): + """Merge a list of holiday objects into this object.""" for hol in hol_list: self.header_list.extend(hol.header_list) self.footer_list.extend(hol.footer_list) @@ -142,11 +145,11 @@ class HolidayProvider(object): def parse_day_record(self, fields): """return tuple (etype,ddef,footer,header,flags) - Remarks: - ddef is either None - or int - or ((y,m,d),) - or ((y,m,d),(y,m,d)) + @note: I{ddef} is one of the following: + None + int + ((y,m,d),) + ((y,m,d),(y,m,d)) """ if len(fields) != 5: raise ValueError("Too many fields: " + str(fields)) @@ -187,7 +190,7 @@ class HolidayProvider(object): return (fields[0],res,fields[2],fields[3],fields[4]) def multi_holiday_tuple(self, date1, date2, header, footer, flags): - """returns Holiday objects for (beginning, end, first_dom, rest)""" + """Returns Holiday objects for (beginning, end, first_dom, rest)""" if header: if self.verbose: header_tuple = (header+'..', '..'+header, '..'+header+'..', None) @@ -319,12 +322,11 @@ class HolidayProvider(object): return self.s_weekend if dow >= 5 else self.s_normal def __call__(self, year, month, dom, dow): - """returns (header,footer,day_style) + """Returns (header,footer,day_style) - Args: - month: month (0-12) - dom: day of month (1-31) - dow: day of week (0-6) + @param month: month (0-12) + @param dom: day of month (1-31) + @param dow: day of week (0-6) """ hol = self.get_holiday(year,month,dom) if hol: diff --git a/lib/plugin.py b/lib/plugin.py index 10bf149..ad23c44 100644 --- a/lib/plugin.py +++ b/lib/plugin.py @@ -18,7 +18,7 @@ # ***************************************** # # -# plugin handling routines # +""" plugin handling routines """ # # # ***************************************** @@ -26,7 +26,14 @@ import sys import os.path import glob -def available_files(parent, dir, fmatch = ""): +def available_files(parent, dir, fmatch = None): + """find parent/dir/*.py files to be used for plugins + + @note: + 1. __init__.py should exist + 2. files starting with underscore are ignored + 3. if fnmatch is defined (base name), it matches a single file + """ good = False res = [] pattern = parent + "/" + dir + "/*.py" @@ -43,7 +50,9 @@ def available_files(parent, dir, fmatch = ""): return res if good else [] def plugin_list(cat): - return available_files(plugin_path[0], cat) + available_files(plugin_path[1], cat) + """return a sequence of available plugins, using L{available_files()} and L{get_plugin_paths()}""" + plugin_paths = get_plugin_paths() + return available_files(plugin_paths[0], cat) + available_files(plugin_paths[1], cat) # cat = lang (category) # longcat = language @@ -51,4 +60,6 @@ def plugin_list(cat): # listopt = --list-lang # preset = "EN" -plugin_path = [ os.path.expanduser("~/.callirhoe"), sys.path[0] if sys.path[0] else "." ] +def get_plugin_paths(): + """return the plugin search paths""" + return [ os.path.expanduser("~/.callirhoe"), sys.path[0] if sys.path[0] else "." ] diff --git a/lib/xcairo.py b/lib/xcairo.py index 15acc4f..12dbf7a 100644 --- a/lib/xcairo.py +++ b/lib/xcairo.py @@ -16,12 +16,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see http://www.gnu.org/licenses/ -# ***************************************** -# # -# general-purpose drawing routines # -# higher-level CAIRO routines # -# # -# ***************************************** +# ******************************************************************** +# # +""" general-purpose drawing routines & higher-level CAIRO routines """ +# # +# ******************************************************************** import cairo import math From e31fa293163dac0bd40789cad7f7f912828cd182 Mon Sep 17 00:00:00 2001 From: "geortz@gmail.com" Date: Thu, 28 Aug 2014 16:35:05 +0000 Subject: [PATCH 14/17] fix bug in bars layout (regarding day of month reference) git-svn-id: https://callirhoe.googlecode.com/svn/branches/next@46 81c8bb96-aa45-f2e2-0eef-c4fa4a15c6df --- layouts/bars.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/layouts/bars.py b/layouts/bars.py index 84fc72e..ad80a36 100644 --- a/layouts/bars.py +++ b/layouts/bars.py @@ -55,7 +55,7 @@ class CalendarRenderer(_base.CalendarRenderer): if dom <= span: holiday_tuple = self.holiday_provider(year, month, dom, day) day_style = holiday_tuple[2] - dcell = _base.DayCell(day = (dom, col), header = holiday_tuple[0], footer = holiday_tuple[1], + dcell = _base.DayCell(day = (dom, day), header = holiday_tuple[0], footer = holiday_tuple[1], theme = (day_style, G.dom, L), show_day_name = True) dcell.draw(cr, R, daycell_thres) else: From d7ec93602d6cbfb24d652c1787cf74080d889e18 Mon Sep 17 00:00:00 2001 From: "geortz@gmail.com" Date: Wed, 3 Sep 2014 12:56:19 +0000 Subject: [PATCH 15/17] full code documentation with epydoc git-svn-id: https://callirhoe.googlecode.com/svn/branches/next@47 81c8bb96-aa45-f2e2-0eef-c4fa4a15c6df --- geom/default.py | 4 + geom/sloppy.py | 2 + holidays/french_holidays.EN.dat | 2 +- lang/DE.py | 2 +- lang/EL.py | 2 + lang/EN.py | 4 +- lang/FR.py | 2 + lang/TR.py | 4 +- layouts/_base.py | 64 +++++++++--- layouts/bars.py | 9 +- layouts/classic.py | 10 +- lib/geom.py | 2 +- lib/holiday.py | 178 +++++++++++++++++++++++--------- lib/xcairo.py | 169 ++++++++++++++++++++++++++---- style/bw.py | 12 ++- style/default.py | 13 ++- style/gfs.py | 4 +- style/rainbow-gfs.py | 1 + style/rainbow.py | 1 + 19 files changed, 384 insertions(+), 101 deletions(-) diff --git a/geom/default.py b/geom/default.py index 8215af8..cd179f8 100644 --- a/geom/default.py +++ b/geom/default.py @@ -16,7 +16,10 @@ # --- geom.default --- +"""module defining the default geometry""" + class dom: + """day of month geometry""" size = (0.5,0.5,0.8,0.5) # short 0-1, long 2-3 mw_split = (0.7,0.2) @@ -27,6 +30,7 @@ class dom: hf_vsplit = (0.5,0.4) class month: + """month geometry""" symmetric = False sloppy_rot = 0 sloppy_dx = 0 diff --git a/geom/sloppy.py b/geom/sloppy.py index 929f6cd..959a701 100644 --- a/geom/sloppy.py +++ b/geom/sloppy.py @@ -16,6 +16,8 @@ # --- geom.sloppy --- +"""module defining the sloppy geometry""" + import default class dom(default.dom): pass diff --git a/holidays/french_holidays.EN.dat b/holidays/french_holidays.EN.dat index 68a85e4..b6b10d2 100644 --- a/holidays/french_holidays.EN.dat +++ b/holidays/french_holidays.EN.dat @@ -30,4 +30,4 @@ ce|39||Ascension|off ce|50||Whit Monday|off d|20130223-20130310|winter vacations (B)||multi -f|20140222-20140309|winter vacations (B)||multi +d|20140222-20140309|winter vacations (B)||multi diff --git a/lang/DE.py b/lang/DE.py index 453f6a3..130a52d 100644 --- a/lang/DE.py +++ b/lang/DE.py @@ -3,7 +3,7 @@ # callirhoe - high quality calendar rendering # Copyright (C) 2012 George M. Tzoumas -# German language data +""" German language definition file""" # Copyright (C) 2013 Neels Hofmeyr # This program is free software: you can redistribute it and/or modify diff --git a/lang/EL.py b/lang/EL.py index 66a4a51..3c81937 100644 --- a/lang/EL.py +++ b/lang/EL.py @@ -16,6 +16,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see http://www.gnu.org/licenses/ +"""Greek language definition file""" + long_day_name = [ u'Δευτέρα', u'Τρίτη', u'Τετάρτη', u'Πέμπτη', u'Παρασκευή', u'Σάββατο', u'Κυριακή' ] diff --git a/lang/EN.py b/lang/EN.py index c86ba4c..cbf1bb1 100644 --- a/lang/EN.py +++ b/lang/EN.py @@ -16,8 +16,10 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see http://www.gnu.org/licenses/ +"""English language definition file""" + long_day_name = [ u'Monday', u'Tuesday', u'Wednesday', - u'Thursday', u'Friday', u'Saturday', u'Sunday' ] + u'Thursday', u'Friday', u'Saturday', u'Sunday' ] short_day_name = [ u'Mo', u'Tu', u'We', u'Th', u'Fr', u'Sa', u'Su' ] diff --git a/lang/FR.py b/lang/FR.py index f11c65f..539ffaa 100644 --- a/lang/FR.py +++ b/lang/FR.py @@ -16,6 +16,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see http://www.gnu.org/licenses/ +"""French language definition file""" + long_day_name = [ u'Lundi', u'Mardi', u'Mercredi', u'Jeudi', u'Vendredi', u'Samedi', u'Dimanche' ] diff --git a/lang/TR.py b/lang/TR.py index bbe646b..626ca94 100644 --- a/lang/TR.py +++ b/lang/TR.py @@ -3,7 +3,7 @@ # callirhoe - high quality calendar rendering # Copyright (C) 2012 George M. Tzoumas -# Turkish language data +""" Turkish language definition file""" # Copyright (C) 2013 Ece Neslihan Aybeke # This program is free software: you can redistribute it and/or modify @@ -24,7 +24,7 @@ long_day_name = [ u'Pazartesi', u'Salı', u' Çarşamba ', short_day_name = [ u'Pt', u'Sa', u'Ça', u'Pe', u'Cu', u'Ct', u'Pa' ] -long_month_name = [ '', u'Ocak', 'Şubat', u'Mart', u'Nisan', +long_month_name = [ '', u'Ocak', u'Şubat', u'Mart', u'Nisan', u'Mayıs', u'Haziran', u'Temmuz', u'Ağustos', u'Eylül', u'Ekim', u'Kasım', u'Aralık' ] diff --git a/layouts/_base.py b/layouts/_base.py index b18646e..57d63fb 100644 --- a/layouts/_base.py +++ b/layouts/_base.py @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see http://www.gnu.org/licenses/ -# --- layouts._base --- +"""base layout module -- others may inherit from this one""" import optparse from lib.xcairo import * @@ -23,6 +23,10 @@ from lib.geom import * from math import floor, ceil, sqrt def get_parser(layout_name): + """get the parser object for the layout command-line arguments + + @param layout_name: corresponding python module (.py file) + """ lname = layout_name.split(".")[1] parser = optparse.OptionParser(usage="%prog (...) --layout " + lname + " [options] (...)",add_help_option=False) parser.add_option("--rows", type="int", default=0, help="force grid rows [%default]") @@ -67,6 +71,16 @@ def get_parser(layout_name): class DayCell(object): + """class Holding a day cell to be drawn + + @type day: int + @ivar day: day of week + @ivar header: header string + @ivar footer: footer string + @ivar theme: (Style,Geometry,Language) tuple + @type show_day_name: bool + @ivar show_day_name: whether day name is displayed + """ def __init__(self, day, header, footer, theme, show_day_name): self.day = day self.header = header @@ -75,6 +89,7 @@ class DayCell(object): self.show_day_name = show_day_name def _draw_short(self, cr, rect): + """render the day cell in short mode""" S,G,L = self.theme x, y, w, h = rect day_of_month, day_of_week = self.day @@ -86,24 +101,25 @@ class DayCell(object): Rdom = R valign = 0 if self.show_day_name else 2 # draw day of month (number) - draw_str(cr, text = str(day_of_month), rect = Rdom, stretch = -1, stroke_rgba = S.fg, + draw_str(cr, text = str(day_of_month), rect = Rdom, scaling = -1, stroke_rgba = S.fg, align = (2,valign), font = S.font, measure = "88") # draw name of day if self.show_day_name: - draw_str(cr, text = L.day_name[day_of_week][0], rect = Rdow, stretch = -1, stroke_rgba = S.fg, + draw_str(cr, text = L.day_name[day_of_week][0], rect = Rdow, scaling = -1, stroke_rgba = S.fg, align = (2,valign), font = S.font, measure = "88") # draw header if self.header: R = rect_rel_scale(rect, G.header_size[0], G.header_size[1], 0, -1.0 + G.header_align) - draw_str(cr, text = self.header, rect = R, stretch = -1, stroke_rgba = S.header, + draw_str(cr, text = self.header, rect = R, scaling = -1, stroke_rgba = S.header, font = S.header_font) # , measure = "MgMgMgMgMgMg" # draw footer if self.footer: R = rect_rel_scale(rect, G.footer_size[0], G.footer_size[1], 0, 1.0 - G.footer_align) - draw_str(cr, text = self.footer, rect = R, stretch = -1, stroke_rgba = S.footer, + draw_str(cr, text = self.footer, rect = R, scaling = -1, stroke_rgba = S.footer, font = S.footer_font) def _draw_long(self, cr, rect): + """render the day cell in long mode""" S,G,L = self.theme x, y, w, h = rect day_of_month, day_of_week = self.day @@ -116,23 +132,27 @@ class DayCell(object): Rdom = rect_rel_scale(R1, G.size[0], G.size[1]) valign = 0 if self.show_day_name else 2 # draw day of month (number) - draw_str(cr, text = str(day_of_month), rect = Rdom, stretch = -1, stroke_rgba = S.fg, + draw_str(cr, text = str(day_of_month), rect = Rdom, scaling = -1, stroke_rgba = S.fg, align = (2,valign), font = S.font, measure = "88") # draw name of day if self.show_day_name: - draw_str(cr, text = L.day_name[day_of_week], rect = Rdow, stretch = -1, stroke_rgba = S.fg, + draw_str(cr, text = L.day_name[day_of_week], rect = Rdow, scaling = -1, stroke_rgba = S.fg, align = (0,valign), font = S.font, measure = "M") Rh, Rf = rect_vsplit(Rhf, *G.hf_vsplit) # draw header if self.header: - draw_str(cr, text = self.header, rect = Rh, stretch = -1, stroke_rgba = S.header, align = (1,2), + draw_str(cr, text = self.header, rect = Rh, scaling = -1, stroke_rgba = S.header, align = (1,2), font = S.header_font) # draw footer if self.footer: - draw_str(cr, text = self.footer, rect = Rf, stretch = -1, stroke_rgba = S.footer, align = (1,2), + draw_str(cr, text = self.footer, rect = Rf, scaling = -1, stroke_rgba = S.footer, align = (1,2), font = S.footer_font) def draw(self, cr, rect, short_thres): + """automatically render a short or long day cell depending on threshold I{short_thres} + + If C{rect} ratio is less than C{short_thres} then short mode is chosen, otherwise long mode. + """ if rect_ratio(rect) < short_thres: self._draw_short(cr, rect) else: @@ -140,6 +160,17 @@ class DayCell(object): class CalendarRenderer(object): + """base monthly calendar renderer - others inherit from this + + @ivar Outfile: output file + @ivar Year: year of first month + @ivar Month: first month + @ivar MonthSpan: month span + @ivar Theme: (Style,Geometry,Language) tuple + @ivar holiday_provider: L{HolidayProvider} object + @ivar version_string: callirhoe version string + @ivar options: parser options object + """ def __init__(self, Outfile, Year, Month, MonthSpan, Theme, holiday_provider, version_string, options): self.Outfile = Outfile self.Year = Year @@ -151,6 +182,14 @@ class CalendarRenderer(object): self.options = options def _draw_month(self, cr, rect, month, year, daycell_thres): + """this method renders a calendar month, it B{should be overridden} in any subclass + + @param cr: cairo context + @param rect: rendering rect + @param month: month + @param year: year + @param daycell_thres: short/long day cell threshold + """ raise NotImplementedError("base _draw_month() should be overridden") #1 1 1 @@ -171,6 +210,7 @@ class CalendarRenderer(object): #rows = 0 #cols = 0 def render(self): + """main calendar rendering routine""" S,G,L = self.Theme rows, cols = self.options.rows, self.options.cols @@ -186,7 +226,7 @@ class CalendarRenderer(object): S.month.color_map_fg = (S.month.color_map_fg[1], S.month.color_map_fg[0]) try: - page = PageWriter(self.Outfile, G.landscape, G.pagespec, G.border, not self.options.opaque) + page = PageWriter(self.Outfile, G.pagespec, not self.options.opaque, G.landscape, G.border) except InvalidFormat as e: print >> sys.stderr, "invalid output format", e.args[0] sys.exit(1) @@ -253,11 +293,11 @@ class CalendarRenderer(object): if (y > yy[-1]): yy.append(y) if not self.options.month_with_year and not self.options.no_footer: year_str = str(yy[0]) if yy[0] == yy[-1] else "%s – %s" % (yy[0],yy[-1]) - draw_str(page.cr, text = year_str, rect = Rc, stroke_rgba = (0,0,0,0.5), stretch = -1, + draw_str(page.cr, text = year_str, rect = Rc, stroke_rgba = (0,0,0,0.5), scaling = -1, align = (0,0), font = (extract_font_name(S.month.font),0,0)) if not self.options.no_footer: draw_str(page.cr, text = "rendered by Callirhoe ver. %s" % self.version_string, - rect = Rc, stroke_rgba = (0,0,0,0.5), stretch = -1, align = (1,0), + rect = Rc, stroke_rgba = (0,0,0,0.5), scaling = -1, align = (1,0), font = (extract_font_name(S.month.font),1,0)) num_pages_written += 1 page.end_page() diff --git a/layouts/bars.py b/layouts/bars.py index ad80a36..c8fe838 100644 --- a/layouts/bars.py +++ b/layouts/bars.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # callirhoe - high quality calendar rendering -# Copyright (C) 2012-2013 George M. Tzoumas +# Copyright (C) 2012-2014 George M. Tzoumas # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see http://www.gnu.org/licenses/ -# --- layouts.bars --- +"""bars layout""" from lib.xcairo import * from lib.geom import * @@ -29,10 +29,11 @@ import _base parser = _base.get_parser(__name__) class CalendarRenderer(_base.CalendarRenderer): + """bars layout class""" #default thres = 2.5 def _draw_month(self, cr, rect, month, year, daycell_thres): S,G,L = self.Theme - apply_rect(cr, rect, G.month.sloppy_dx, G.month.sloppy_dy, G.month.sloppy_rot) + make_sloppy_rect(cr, rect, G.month.sloppy_dx, G.month.sloppy_dy, G.month.sloppy_rot) day, span = calendar.monthrange(year, month) mmeasure = 'A'*max(map(len,L.month_name)) @@ -79,7 +80,7 @@ class CalendarRenderer(_base.CalendarRenderer): mshad = (f,-f) if G.landscape else (f,f) title_str = L.month_name[month] if self.options.month_with_year: title_str += ' ' + str(year) - draw_str(cr, text = title_str, rect = R_text, stretch = -1, stroke_rgba = mcolor_fg, + draw_str(cr, text = title_str, rect = R_text, scaling = -1, stroke_rgba = mcolor_fg, align = (2,0), font = S.month.font, measure = mmeasure, shadow = mshad) cr.restore() diff --git a/layouts/classic.py b/layouts/classic.py index ab63c4e..dba88e1 100644 --- a/layouts/classic.py +++ b/layouts/classic.py @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see http://www.gnu.org/licenses/ -# --- layouts.classic --- +"""classic layout""" from lib.xcairo import * from lib.geom import * @@ -28,6 +28,7 @@ import _base parser = _base.get_parser(__name__) def _weekrows_of_month(year, month): + """returns the number of Monday-Sunday ranges (or subsets of) that a month contains, which are 4, 5 or 6""" day,span = calendar.monthrange(year, month) if day == 0 and span == 28: return 4 if day == 5 and span == 31: return 6 @@ -35,10 +36,11 @@ def _weekrows_of_month(year, month): return 5 class CalendarRenderer(_base.CalendarRenderer): + """classic tiles layout class""" #default thres = 2.5 def _draw_month(self, cr, rect, month, year, daycell_thres): S,G,L = self.Theme - apply_rect(cr, rect, G.month.sloppy_dx, G.month.sloppy_dy, G.month.sloppy_rot) + make_sloppy_rect(cr, rect, G.month.sloppy_dx, G.month.sloppy_dy, G.month.sloppy_rot) day, span = calendar.monthrange(year, month) weekrows = 6 if G.month.symmetric else _weekrows_of_month(year, month) @@ -67,7 +69,7 @@ class CalendarRenderer(_base.CalendarRenderer): fill_rgba = S.dom.bg if col < 5 else S.dom_weekend.bg, stroke_width = mm_to_dots(S.dow.frame_thickness)) R_text = rect_rel_scale(R, 1, 0.5) - draw_str(cr, text = L.day_name[col], rect = R_text, stretch = -1, stroke_rgba = S.dow.fg, + draw_str(cr, text = L.day_name[col], rect = R_text, scaling = -1, stroke_rgba = S.dow.fg, align = (2,0), font = S.dow.font, measure = wmeasure) # draw day cells @@ -100,7 +102,7 @@ class CalendarRenderer(_base.CalendarRenderer): mshad = (f,-f) if G.landscape else (f,f) title_str = L.month_name[month] if self.options.month_with_year: title_str += ' ' + str(year) - draw_str(cr, text = title_str, rect = R_text, stretch = -1, stroke_rgba = mcolor_fg, + draw_str(cr, text = title_str, rect = R_text, scaling = -1, stroke_rgba = mcolor_fg, align = (2,0), font = S.month.font, measure = mmeasure, shadow = mshad) cr.restore() diff --git a/lib/geom.py b/lib/geom.py index beea089..3f7918c 100644 --- a/lib/geom.py +++ b/lib/geom.py @@ -170,7 +170,7 @@ class HLayout(VLayout): t = super(HLayout,self).item(i) return (t[1], t[0], t[3], t[2]) -class GLayout: +class GLayout(object): """grid layout manager @ivar vrep: internal L{VLayout} for row computations diff --git a/lib/holiday.py b/lib/holiday.py index ac0b8f1..dad646d 100644 --- a/lib/holiday.py +++ b/lib/holiday.py @@ -25,7 +25,7 @@ from datetime import date, timedelta def _get_orthodox_easter(year): - """Compute date of orthodox easter.""" + """compute date of orthodox easter""" y1, y2, y3 = year % 4 , year % 7, year % 19 a = 19*y3 + 15 y4 = a % 30 @@ -37,7 +37,7 @@ def _get_orthodox_easter(year): # return res def _get_catholic_easter(year): - """Compute date of catholic easter.""" + """compute date of catholic easter""" a, b, c = year % 19, year // 100, year % 100 d, e = divmod(b,4) f = (b + 8) // 25 @@ -49,11 +49,23 @@ def _get_catholic_easter(year): emonth,edate = divmod(h + l - 7*m + 114,31) return date(year, emonth, edate+1) -class Holiday(object): - """class holding a Holiday object (date is I{not} stored!) +def _strip_empty(sl): + """strip empty strings from list I{sl}""" + return filter(lambda z: z, sl) if sl else [] - @ivar header: string for header (primary text) - @ivar footer: string for footer (secondary text) +def _flatten(sl): + """join list I{sl} into a comma-separated string""" + if not sl: return None + res = sl[0] + for s in sl[1:]: + res += ', ' + s + return res + +class Holiday(object): + """class holding a Holiday object (date is I{not} stored, use L{HolidayProvider} for that) + + @ivar header_list: string list for header (primary text) + @ivar footer_list: string list for footer (secondary text) @ivar flags: bit combination of {OFF=1, MULTI=2, REMINDER=4} I{OFF}: day off (real holiday) @@ -73,27 +85,31 @@ class Holiday(object): MULTI = 2 REMINDER = 4 def __init__(self, header = [], footer = [], flags_str = None): - self.header_list = self._strip_empty(header) - self.footer_list = self._strip_empty(footer) + self.header_list = _strip_empty(header) + self.footer_list = _strip_empty(footer) self.flags = self._parse_flags(flags_str) def merge_with(self, hol_list): - """Merge a list of holiday objects into this object.""" + """merge a list of holiday objects into this object""" for hol in hol_list: self.header_list.extend(hol.header_list) self.footer_list.extend(hol.footer_list) self.flags |= hol.flags def header(self): - return self._flatten(self.header_list) + """return a comma-separated string for L{header_list}""" + return _flatten(self.header_list) def footer(self): - return self._flatten(self.footer_list) + """return a comma-separated string for L{footer_list}""" + return _flatten(self.footer_list) - def __repr__(self): + def __str__(self): + """string representation for debugging purposes""" return str(self.footer()) + ':' + str(self.header()) + ':' + str(self.flags) def _parse_flags(self, fstr): + """return a bit combination of flags, from a comma-separated string list""" if not fstr: return 0 fs = fstr.split(',') val = 0 @@ -104,18 +120,20 @@ class Holiday(object): elif 'reminder'.startswith(s): val |= Holiday.REMINDER return val - def _strip_empty(self, sl): - return filter(lambda z: z, sl) if sl else [] - - def _flatten(self, sl): - if not sl: return None - res = sl[0] - for s in sl[1:]: - res += ', ' + s - return res - - def _decode_date_str(ddef): + """decode a date definition string into a I{(year,month,day)} tuple + + @param ddef: date definition string of length 2, 4 or 8 + + If C{ddef} is of the form "DD" then tuple (0,0,DD) is returned, which + stands for any year - any month - day DD. + + If C{ddef} is of the form "MMDD" then tuple (0,MM,DD) is returned, which + stands for any year - month MM - day DD. + + If C{ddef} is of the form "YYYYMMDD" then tuple (YYYY,MM,DD) is returned, which + stands for year YYYY - month MM - day DD. + """ if len(ddef) == 2: return (0,0,int(ddef)) if len(ddef) == 4: @@ -125,7 +143,36 @@ def _decode_date_str(ddef): raise ValueError("invalid date definition '%s'" % ddef) class HolidayProvider(object): - def __init__(self, s_normal, s_weekend, s_holiday, s_weekend_holiday, s_multi, s_weekend_multi, verbose=True): + """class holding the holidays throught the year(s) + + @ivar annual: dict of events occuring annually, indexed by tuple I{(day,month)}. Note + each dict entry is actually a list of L{Holiday} objects. This is also true for the other + instance variables: L{monthly}, L{fixed}, L{orth_easter}, L{george}, L{cath_easter}. + @ivar monthly: event occuring monthly, indexed by int I{day} + @ivar fixed: fixed date events, indexed by a C{date()} object + @ivar orth_easter: dict of events relative to the orthodox easter Sunday, indexed by + an integer days offset + @ivar george: events occuring on St George's day (orthodox calendar special computation) + @ivar cath_easter: dict of events relative to the catholic easter Sunday, indexed by + an integer days offset + @ivar cache: for each year requested, all holidays occuring + within that year (annual, monthly, easter-based etc.) are precomputed and stored into + dict C{cache}, indexed by a C{date()} object + @ivar ycache: set holding cached years; each new year requested, triggers a cache-fill + operation + """ + def __init__(self, s_normal, s_weekend, s_holiday, s_weekend_holiday, s_multi, s_weekend_multi, multiday_markers=True): + """initialize a C{HolidayProvider} object + + @param s_normal: style class object for normal (weekday) day cells + @param s_weekend: style for weekend day cells + @param s_holiday: style for holiday day cells + @param s_weekend_holiday: style for holiday cells on weekends + @param s_multi: style for multi-day holiday weekday cells + @param s_weekend_multi: style for multi-day holiday weekend cells + @param multiday_markers: if C{True}, then use end-of-multiday-holiday markers and range markers (with dots), + otherwise only first day and first-day-of-month are marked + """ self.annual = dict() # key = (d,m) self.monthly = dict() # key = d self.fixed = dict() # key = date() @@ -140,9 +187,9 @@ class HolidayProvider(object): self.s_weekend_holiday = s_weekend_holiday self.s_multi = s_multi self.s_weekend_multi = s_weekend_multi - self.verbose = verbose + self.multiday_markers = multiday_markers - def parse_day_record(self, fields): + def _parse_day_record(self, fields): """return tuple (etype,ddef,footer,header,flags) @note: I{ddef} is one of the following: @@ -189,17 +236,22 @@ class HolidayProvider(object): res = int(fields[1]) return (fields[0],res,fields[2],fields[3],fields[4]) - def multi_holiday_tuple(self, date1, date2, header, footer, flags): - """Returns Holiday objects for (beginning, end, first_dom, rest)""" + def _multi_holiday_tuple(self, header, footer, flags): + """returns a 4-tuple of L{Holiday} objects representing (beginning, end, first-day-of-month, rest) + + @param header: passed as C{[header]} of the generated L{Holiday} object + @param footer: passed as C{[footer]} of the generated L{Holiday} object + @param flags: C{flags} of the generated L{Holiday} object + """ if header: - if self.verbose: + if self.multiday_markers: header_tuple = (header+'..', '..'+header, '..'+header+'..', None) else: header_tuple = (header, None, header, None) else: header_tuple = (None, None, None, None) if footer: - if self.verbose: + if self.multiday_markers: footer_tuple = (footer+'..', '..'+footer, '..'+footer+'..', None) else: footer_tuple = (footer, None, footer, None) @@ -208,29 +260,47 @@ class HolidayProvider(object): return tuple(map(lambda k: Holiday([header_tuple[k]], [footer_tuple[k]], flags), range(4))) - # File Format: - # type|DATE*span|footer|header|flags - # type|DATE1-DATE2|footer|header|flags - # type|DATE|footer|header|flags - # - # type: - # d: event occurs annually fixed day/month: MMDD - # d: event occurs monthly, fixed day: DD - # d: fixed day/month/year combination (e.g. deadline, trip, etc.): YYYYMMDD - # oe: Orthodox Easter-dependent holiday, annually - # ge: Georgios' name day, Orthodox Easter dependent holiday, annually - # ce: Catholic Easter holiday - # - # DATE*span and DATE1-DATE2 supported only for YYYYMMDD - # flags = {off, multi} def load_holiday_file(self, filename): + """load a holiday file into the C{HolidayProvider} object + + B{File Format:} + - C{type|date*span|footer|header|flags} + - C{type|date1-date2|footer|header|flags} + - C{type|date|footer|header|flags} + + I{type:} + - C{d}: event occurs annually fixed day/month; I{date}=MMDD + - C{d}: event occurs monthly, fixed day; I{date}=DD + - C{d}: fixed day/month/year combination (e.g. deadline, trip, etc.); I{date}=YYYYMMDD + - C{oe}: Orthodox Easter-dependent holiday, annually; I{date}=integer offset in days + - C{ge}: Georgios' name day, Orthodox Easter dependent holiday, annually; I{date} field is ignored + - C{ce}: Catholic Easter holiday; I{date}=integer offset in days + + I{date*span} and range I{date1-date2} supported only for I{date}=YYYYMMDD (fixed) events + + I{flags:} comma-separated list of the following: + 1. off + 2. multi + 3. reminder (or any prefix of it) + + B{Example}:: + + d|0101||New year's|off + d|0501||Labour day|off + ce|-2||Good Friday| + ce|0||Easter|off + ce|1||Easter Monday|off + d|20130223-20130310|winter vacations (B)||multi + + @param filename: file to be loaded + """ with open(filename, 'r') as f: for line in f: line = line.strip() if not line: continue if line[0] == '#': continue fields = line.split('|') - etype,ddef,footer,header,flags = self.parse_day_record(fields) + etype,ddef,footer,header,flags = self._parse_day_record(fields) hol = Holiday([header], [footer], flags) if etype == 'd': if len(ddef) == 1: @@ -249,7 +319,7 @@ class HolidayProvider(object): self.fixed[dt1].append(hol) else: # properly annotate multi-day events - hols = self.multi_holiday_tuple(dt1, dt2, header, footer, flags) + hols = self._multi_holiday_tuple(header, footer, flags) dt = dt1 while dt <= dt2: if dt not in self.fixed: self.fixed[dt] = [] @@ -273,6 +343,11 @@ class HolidayProvider(object): self.cath_easter[d].append(hol) def get_holiday(self, y, m, d): + """return a L{Holiday} object for the specified date (y,m,d) or C{None} if no holiday is defined + + @note: If year I{y} has not been requested before, the cache is updated first + with all holidays that belong in I{y}, indexed by C{date()} objects. + """ if y not in self.ycache: # fill-in events for year y # annual @@ -315,6 +390,11 @@ class HolidayProvider(object): return self.cache[dt] if dt in self.cache else None def get_style(self, flags, dow): + """return appropriate style object, depending on I{flags} and I{dow} + + @param flags: bit combination of holiday flags + @param dow: day of week + """ if flags & Holiday.OFF: return self.s_weekend_holiday if dow >= 5 else self.s_holiday if flags & Holiday.MULTI: @@ -322,7 +402,7 @@ class HolidayProvider(object): return self.s_weekend if dow >= 5 else self.s_normal def __call__(self, year, month, dom, dow): - """Returns (header,footer,day_style) + """returns (header,footer,day_style) @param month: month (0-12) @param dom: day of month (1-31) @@ -337,6 +417,8 @@ class HolidayProvider(object): if __name__ == '__main__': import sys hp = HolidayProvider('n', 'w', 'h', 'wh', 'm', 'wm') + if len(sys.argv) < 3: + raise SystemExit("Usage: %s YEAR holiday_file ..." % sys.argv[0]); y = int(sys.argv[1]) for f in sys.argv[2:]: hp.load_holiday_file(f) diff --git a/lib/xcairo.py b/lib/xcairo.py index 12dbf7a..2fcb387 100644 --- a/lib/xcairo.py +++ b/lib/xcairo.py @@ -29,16 +29,30 @@ from os.path import splitext from geom import * XDPI = 72.0 +"""dots per inch of output device""" + +# decreasing order +# [1188, 840, 594, 420, 297, 210, 148, 105, 74, 52, 37] ISOPAGE = map(lambda x: int(210*math.sqrt(2)**x+0.5), range(5,-6,-1)) +"""ISO page height list, index k for height of Ak paper""" def page_spec(spec = None): + """return tuple of page dimensions (width,height) in mm for I{spec} + + @param spec: paper type. + Paper type can be an ISO paper type (a0..a9 or a0w..a9w) or of the + form W:H; positive values correspond to W or H mm, negative values correspond to + -W or -H pixels; 'w' suffix swaps width & height; None defaults to A4 paper + """ if not spec: return (ISOPAGE[5], ISOPAGE[4]) if len(spec) == 2 and spec[0].lower() == 'a': k = int(spec[1]) + if k > 9: k = 9 return (ISOPAGE[k+1], ISOPAGE[k]) if len(spec) == 3 and spec[0].lower() == 'a' and spec[2].lower() == 'w': k = int(spec[1]) + if k > 9: k = 9 return (ISOPAGE[k], ISOPAGE[k+1]) if ':' in spec: s = spec.split(':') @@ -48,13 +62,38 @@ def page_spec(spec = None): return (w,h) def mm_to_dots(mm): + """convert millimeters to dots""" return mm/25.4 * XDPI def dots_to_mm(dots): + """convert dots to millimeters""" return dots*25.4/XDPI class Page(object): + """class holding Page properties + + @type Size_mm: tuple (width,height) + @ivar Size_mm: page dimensions in mm + @type landscape: bool + @ivar landscape: landscape mode (for landscape, Size_mm will have swapped elements) + @type Size: tuple (width,height) + @ivar Size: page size in dots/pixels + @type Margins: tuple (top,left,bottom,right) + @ivar Margins: page margins in pixels + @type Text_rect: tuple (x,y,w,h) + @ivar Text_rect: text rectangle + """ def __init__(self, landscape, w, h, b, raster): + """initialize Page properties object + + @type landscape: bool + @param landscape: landscape mode + @param w: page physical width in mm + @param h: page physical height in mm, M{h>w}, even in landscape mode + @param b: page border in mm (uniform) + @type raster: bool + @param raster: raster mode (not vector) + """ if not landscape: self.Size_mm = (w, h) # (width, height) in mm else: @@ -67,12 +106,37 @@ class Page(object): self.Size[1] - self.Margins[0] - self.Margins[2]) self.Text_rect = (self.Margins[1], self.Margins[0], txs[0], txs[1]) -class InvalidFormat(Exception): pass +class InvalidFormat(Exception): + """exception thrown when an invalid output format is requested""" + pass class PageWriter(Page): + """class to output multiple pages in raster (png) or vector (pdf) format + + + @ivar base: out filename (without extension) + @ivar ext: filename extension (with dot) + @type curpage: int + @ivar curpage: current page + @ivar format: output format: L{PDF} or L{PNG} + @type keep_transparency: bool + @ivar keep_transparency: C{True} to use transparent instead of white fill color + @ivar img_format: C{cairo.FORMAT_ARGB32} or C{cairo.FORMAT_RGB24} depending on + L{keep_transparency} + @ivar Surface: cairo surface (set by L{_setup_surface_and_context}) + @ivar cr: cairo context (set by L{_setup_surface_and_context}) + """ + PDF = 0 PNG = 1 - def __init__(self, filename, landscape = False, pagespec = None, b = 0.0, keep_transparency = True): + def __init__(self, filename, pagespec = None, keep_transparency = True, landscape = False, b = 0.0): + """initialize PageWriter object + + see also L{Page.__init__} + @param filename: output filename (extension determines format PDF or PNG) + @param pagespec: iso page spec, see L{page_spec} + @param keep_transparency: see L{keep_transparency} + """ self.base,self.ext = splitext(filename) self.filename = filename self.curpage = 1 @@ -90,19 +154,15 @@ class PageWriter(Page): w, h = h, w landscape = False super(PageWriter,self).__init__(landscape, w, h, b, self.format == PageWriter.PNG) - self.setup_surface_and_context() + self._setup_surface_and_context() - def setup_surface_and_context(self): - if not self.landscape: - if self.format == PageWriter.PDF: - self.Surface = cairo.PDFSurface(self.filename, self.Size[0], self.Size[1]) - else: - self.Surface = cairo.ImageSurface(self.img_format, int(self.Size[0]), int(self.Size[1])) + def _setup_surface_and_context(self): + """setup cairo surface taking into account raster mode, transparency and landscape mode""" + z = int(self.landscape) + if self.format == PageWriter.PDF: + self.Surface = cairo.PDFSurface(self.filename, self.Size[z], self.Size[1-z]) else: - if self.format == PageWriter.PDF: - self.Surface = cairo.PDFSurface(self.filename, self.Size[1], self.Size[0]) - else: - self.Surface = cairo.ImageSurface(self.img_format, int(self.Size[1]), int(self.Size[0])) + self.Surface = cairo.ImageSurface(self.img_format, int(self.Size[z]), int(self.Size[1-z])) self.cr = cairo.Context(self.Surface) if self.landscape: @@ -118,11 +178,13 @@ class PageWriter(Page): self.cr.fill() def end_page(self): + """in PNG mode, output a separate file for each page""" if self.format == PageWriter.PNG: outfile = self.filename if self.curpage < 2 else self.base + "%02d" % (self.curpage) + self.ext self.Surface.write_to_png(outfile) def new_page(self): + """setup next page""" if self.format == PageWriter.PDF: self.cr.show_page() else: @@ -131,15 +193,29 @@ class PageWriter(Page): def set_color(cr, rgba): + """set stroke color + + @param cr: cairo context + @type rgba: tuple + @param rgba: (r,g,b) or (r,g,b,a) + """ if len(rgba) == 3: cr.set_source_rgb(*rgba) else: cr.set_source_rgba(*rgba) def extract_font_name(f): + "extract the font name from a string or from a tuple (fontname, slant, weight)""" return f if type(f) is str else f[0] -def apply_rect(cr, rect, sdx = 0.0, sdy = 0.0, srot = 0.0): +def make_sloppy_rect(cr, rect, sdx = 0.0, sdy = 0.0, srot = 0.0): + """slightly rotate and translate a rect to give it a sloppy look + + @param cr: cairo context + @param sdx: maximum x-offset, true offset will be uniformly distibuted + @param sdy: maximum y-offset + @param sdy: maximum rotation + """ x, y, w, h = rect cr.save() cr.translate(x,y) @@ -150,6 +226,15 @@ def apply_rect(cr, rect, sdx = 0.0, sdy = 0.0, srot = 0.0): cr.translate(-w/2.0, -h/2.0) def draw_shadow(cr, rect, thickness = None, shadow_color = (0,0,0,0.3)): + """draw a shadow at the bottom-right corner of a rect + + @param cr: cairo context + @param rect: tuple (x,y,w,h) + @param thickness: if C{None} nothing is drawn + @param shadow_color: shadow color + """ + + if thickness is None: return fx = mm_to_dots(thickness[0]) fy = mm_to_dots(thickness[1]) @@ -172,6 +257,13 @@ def draw_shadow(cr, rect, thickness = None, shadow_color = (0,0,0,0.3)): cr.close_path(); cr.fill(); def draw_line(cr, rect, stroke_rgba = None, stroke_width = 1.0): + """draw a line from (x,y) to (x+w,y+h), where rect=(x,y,w,h) + + @param cr: cairo context + @param rect: tuple (x,y,w,h) + @param stroke_rgba: stroke color + @param stroke_width: stroke width, if <= 0 nothing is drawn + """ if (stroke_width <= 0): return x, y, w, h = rect cr.move_to(x, y) @@ -183,6 +275,15 @@ def draw_line(cr, rect, stroke_rgba = None, stroke_width = 1.0): cr.stroke() def draw_box(cr, rect, stroke_rgba = None, fill_rgba = None, stroke_width = 1.0, shadow = None): + """draw a box (rectangle) with optional shadow + + @param cr: cairo context + @param rect: box rectangle as tuple (x,y,w,h) + @param stroke_rgba: stroke color (set if not C{None}) + @param fill_rgba: fill color (set if not C{None}) + @param stroke_width: stroke width + @param shadow: shadow thickness + """ if (stroke_width <= 0): return draw_shadow(cr, rect, shadow) x, y, w, h = rect @@ -199,8 +300,34 @@ def draw_box(cr, rect, stroke_rgba = None, fill_rgba = None, stroke_width = 1.0, cr.set_line_width(stroke_width) cr.stroke() -def draw_str(cr, text, rect, stretch = -1, stroke_rgba = None, align = (2,0), bbox = False, +def draw_str(cr, text, rect, scaling = -1, stroke_rgba = None, align = (2,0), bbox = False, font = "Times", measure = None, shadow = None): + """draw text + + @param cr: cairo context + @param text: text string to be drawn + @type scaling: int + @param scaling: text scaling mode + + - -1: auto select x-scaling or y-scaling (whatever fills the rect) + - 0: no scaling + - 1: x-scaling, scale so that text fills rect horizontally, preserving ratio + - 2: y-scaling, scale so that text fills rect vertically, preserving ratio + - 3: xy-scaling, stretch so that text fills rect completely, does not preserve ratio + + @param stroke_rgba: stroke color + @type align: tuple + @param align: alignment mode as (int,int) tuple for horizontal/vertical alignment + + - 0: left/top alignment + - 1: right/bottom alignment + - 2: center/middle alignment + + @param bbox: draw text bounding box (for debugging) + @param font: font name as string or (font,slant,weight) tuple + @param measure: use this string for measurement instead of C{text} + @param shadow: draw text shadow as tuple (dx,dy) + """ x, y, w, h = rect cr.save() slant = weight = 0 @@ -216,13 +343,13 @@ def draw_str(cr, text, rect, stretch = -1, stroke_rgba = None, align = (2,0), bb mw = 5. if mh < 5: mh = 5. - ratio, tratio = w*1.0/h, mw*1.0/mh; + #ratio, tratio = w*1.0/h, mw*1.0/mh; xratio, yratio = mw*1.0/w, mh*1.0/h; - if stretch < 0: stretch = 1 if xratio >= yratio else 2 - if stretch == 0: crs = (1,1) - elif stretch == 1: crs = (1.0/xratio, 1.0/xratio) - elif stretch == 2: crs = (1.0/yratio, 1.0/yratio) - elif stretch == 3: crs = (1.0/xratio, 1.0/yratio) + if scaling < 0: scaling = 1 if xratio >= yratio else 2 + if scaling == 0: crs = (1,1) + elif scaling == 1: crs = (1.0/xratio, 1.0/xratio) + elif scaling == 2: crs = (1.0/yratio, 1.0/yratio) + elif scaling == 3: crs = (1.0/xratio, 1.0/yratio) te = cr.text_extents(text) tw,th = te[2], te[3] tw *= crs[0] diff --git a/style/bw.py b/style/bw.py index 3a0b702..db61a82 100644 --- a/style/bw.py +++ b/style/bw.py @@ -16,15 +16,17 @@ # --- style.bw --- -# day of week +"""module defining the black-& white style""" + class dow: + """day of week style""" fg = (.33,.33,.33) frame_thickness = 0.1 frame = (0.8,0.8,0.8) font = "Arial" -# day of month class dom: + """day of month style""" bg = (1,1,1) frame = (0.8,0.8,0.8) frame_thickness = 0.1 @@ -35,27 +37,33 @@ class dom: header_font = footer_font = "Arial" class dom_weekend(dom): + """day of month style (weekend)""" bg = (0.95,0.95,0.95) fg = (0,0,0) font = ("Times New Roman", 0, 1) class dom_holiday(dom): + """day of month (holiday, indicated by the OFF flag in the holiday file)""" fg = (0,0,0) bg = (0.95,0.95,0.95) header = (0,0,0) font = ("Times New Roman", 0, 1) class dom_weekend_holiday(dom_holiday): + """day of month (weekend & holiday)""" fg = (0,0,0) bg = (0.95,0.95,0.95) class dom_multi(dom_holiday): + """day of month (multi-day holiday)""" pass class dom_weekend_multi(dom_weekend_holiday): + """day of month (weekend in multi-day holiday)""" pass class month: + """month style""" font = ("Times New Roman", 0, 1) frame = (0,0,0) frame_thickness = 0.2 diff --git a/style/default.py b/style/default.py index 56510b8..4921031 100644 --- a/style/default.py +++ b/style/default.py @@ -16,15 +16,17 @@ # --- style.default --- -# day of week +"""module defining the default style""" + class dow: + """day of week style""" fg = (0,0,1) frame_thickness = 0.1 frame = (0.75,0.75,0.75) font = "Arial" -# day of month class dom: + """day of month style""" bg = (1,1,1) frame = (0.75,0.75,0.75) frame_thickness = 0.1 @@ -35,25 +37,30 @@ class dom: header_font = footer_font = "Arial" class dom_weekend(dom): + """day of month style (weekend)""" bg = (0.7,1,1) fg = (0,0,1) -# OFF flag class dom_holiday(dom): + """day of month (holiday, indicated by the OFF flag in the holiday file)""" bg = (0.7,1,1) fg = (1,0,0) header = (1,0,0) class dom_weekend_holiday(dom_holiday): + """day of month (weekend & holiday)""" pass class dom_multi(dom): + """day of month (multi-day holiday)""" bg = (0.7,1,1) class dom_weekend_multi(dom_multi): + """day of month (weekend in multi-day holiday)""" pass class month: + """month style""" font = ("Times New Roman", 0, 1) frame = (0,0,0) frame_thickness = 0.2 diff --git a/style/gfs.py b/style/gfs.py index 0958e6c..10a6933 100644 --- a/style/gfs.py +++ b/style/gfs.py @@ -14,7 +14,9 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see http://www.gnu.org/licenses/ -# --- style.gfs (Greek Font Society fonts) --- +# --- style.gfs --- + +"""module defining Greek Font Society fonts for default style""" import default diff --git a/style/rainbow-gfs.py b/style/rainbow-gfs.py index 2e3d0f8..e47f0bf 100644 --- a/style/rainbow-gfs.py +++ b/style/rainbow-gfs.py @@ -16,6 +16,7 @@ # --- style.rainbow --- +"""module defining rainbow color & gfs style""" import gfs diff --git a/style/rainbow.py b/style/rainbow.py index aad7eba..e202bff 100644 --- a/style/rainbow.py +++ b/style/rainbow.py @@ -16,6 +16,7 @@ # --- style.rainbow --- +"""module defining rainbow color style""" import default From 5741066ae64bd3764fee4b7959e0571830395a5d Mon Sep 17 00:00:00 2001 From: "geortz@gmail.com" Date: Wed, 3 Sep 2014 13:10:44 +0000 Subject: [PATCH 16/17] more pythonic string joining git-svn-id: https://callirhoe.googlecode.com/svn/branches/next@48 81c8bb96-aa45-f2e2-0eef-c4fa4a15c6df --- lib/holiday.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/holiday.py b/lib/holiday.py index dad646d..e39d240 100644 --- a/lib/holiday.py +++ b/lib/holiday.py @@ -56,10 +56,7 @@ def _strip_empty(sl): def _flatten(sl): """join list I{sl} into a comma-separated string""" if not sl: return None - res = sl[0] - for s in sl[1:]: - res += ', ' + s - return res + return ', '.join(sl) class Holiday(object): """class holding a Holiday object (date is I{not} stored, use L{HolidayProvider} for that) From bf01d96f924c6f8bc76e8ae47f04c2ab4dffac14 Mon Sep 17 00:00:00 2001 From: "geortz@gmail.com" Date: Wed, 3 Sep 2014 21:18:16 +0000 Subject: [PATCH 17/17] merged/adapted code for Neels Homfeyr's sparse layout: - sparse layout - sparse style - german/berlin holidays git-svn-id: https://callirhoe.googlecode.com/svn/branches/next@49 81c8bb96-aa45-f2e2-0eef-c4fa4a15c6df --- callirhoe.py | 7 +- lang/DE.py | 2 + lang/EL.py | 2 + lang/EN.py | 1 + lang/FR.py | 2 + lang/TR.py | 2 + layouts/_base.py | 9 +-- layouts/bars.py | 5 +- layouts/classic.py | 5 +- layouts/sparse.py | 185 +++++++++++++++++++++++++++++++++++++++++++++ style/bw.py | 2 +- style/bw_sparse.py | 75 ++++++++++++++++++ 12 files changed, 283 insertions(+), 14 deletions(-) create mode 100644 layouts/sparse.py create mode 100644 style/bw_sparse.py diff --git a/callirhoe.py b/callirhoe.py index 48024c8..a1ade91 100755 --- a/callirhoe.py +++ b/callirhoe.py @@ -24,12 +24,13 @@ # default values for rows/cols depending on layout (classic/bars) # fix auto-measure rendering (cairo) # fix plugin loading (without global vars) +# week markers selectable +# test layouts # allow to change background color (fill), other than white # page spec parse errors # mobile themes (e.g. 800x480) # photo support (like ImageMagick's polaroid effect) -# python source documentation # .callirhoe/config : default values for plugins (styles/layouts/lang...) and cmdline # MAYBE-TODO: @@ -181,6 +182,8 @@ if __name__ == "__main__": help="user the short version of month names (defined in language file) [%default]") parser.add_option("--long-daynames", action="store_true", default=False, help="user the long version of day names (defined in language file) [%default]") + parser.add_option("-T", "--terse-holidays", action="store_false", dest="multiday_holidays", + default=True, help="do not print holiday end markers and omit dots") for x in ["languages", "layouts", "styles", "geometries"]: add_list_option(parser, x) @@ -301,7 +304,7 @@ if __name__ == "__main__": hprovider = holiday.HolidayProvider(Style.dom, Style.dom_weekend, Style.dom_holiday, Style.dom_weekend_holiday, - Style.dom_multi, Style.dom_weekend_multi) + Style.dom_multi, Style.dom_weekend_multi, options.multiday_holidays) if options.holidays: for f in options.holidays: diff --git a/lang/DE.py b/lang/DE.py index 130a52d..4ad3923 100644 --- a/lang/DE.py +++ b/lang/DE.py @@ -32,3 +32,5 @@ long_month_name = [ '', short_month_name = [ '', u'Jan', u'Feb', u'Mrz', u'Apr', u'Mai', u'Jun', u'Jul', u'Aug', u'Sep', u'Okt', u'Nov', u'Dez' ] + +week_of_year_prefix = u'W' diff --git a/lang/EL.py b/lang/EL.py index 3c81937..8eebeb3 100644 --- a/lang/EL.py +++ b/lang/EL.py @@ -29,3 +29,5 @@ long_month_name = [ '', u'Ιανουάριος', u'Φεβρουάριος', u'Μ short_month_name = [ '', u'Ιαν', u'Φεβ', u'Μαρ', u'Απρ', u'Μαϊ', u'Ιον', u'Ιολ', u'Αυγ', u'Σεπ', u'Οκτ', u'Νοε', u'Δεκ' ] + +week_of_year_prefix = u'Ε' diff --git a/lang/EN.py b/lang/EN.py index cbf1bb1..7a671b6 100644 --- a/lang/EN.py +++ b/lang/EN.py @@ -32,3 +32,4 @@ short_month_name = [ '', u'Jan', u'Feb', u'Mar', u'Apr', u'May', u'Jun', u'Jul', u'Aug', u'Sep', u'Oct', u'Nov', u'Dec' ] +week_of_year_prefix = u'W' diff --git a/lang/FR.py b/lang/FR.py index 539ffaa..0e73290 100644 --- a/lang/FR.py +++ b/lang/FR.py @@ -30,3 +30,5 @@ long_month_name = [ '', short_month_name = [ '', u'Jan', u'Fév', u'Mar', u'Avr', u'Mai', u'Jun', u'Jul', u'Aoû', u'Sep', u'Oct', u'Nov', u'Déc' ] + +week_of_year_prefix = u'S' diff --git a/lang/TR.py b/lang/TR.py index 626ca94..77491ae 100644 --- a/lang/TR.py +++ b/lang/TR.py @@ -29,3 +29,5 @@ long_month_name = [ '', u'Ocak', u'Şubat', u'Mart', u'Nisan', u'Eylül', u'Ekim', u'Kasım', u'Aralık' ] short_month_name = long_month_name + +week_of_year_prefix = u'H' diff --git a/layouts/_base.py b/layouts/_base.py index 57d63fb..c0fe3c7 100644 --- a/layouts/_base.py +++ b/layouts/_base.py @@ -77,7 +77,7 @@ class DayCell(object): @ivar day: day of week @ivar header: header string @ivar footer: footer string - @ivar theme: (Style,Geometry,Language) tuple + @ivar theme: (Style class,Geometry class,Language module) tuple @type show_day_name: bool @ivar show_day_name: whether day name is displayed """ @@ -166,7 +166,7 @@ class CalendarRenderer(object): @ivar Year: year of first month @ivar Month: first month @ivar MonthSpan: month span - @ivar Theme: (Style,Geometry,Language) tuple + @ivar Theme: (Style module,Geometry module,Language module) tuple @ivar holiday_provider: L{HolidayProvider} object @ivar version_string: callirhoe version string @ivar options: parser options object @@ -181,14 +181,13 @@ class CalendarRenderer(object): self.version_string = version_string self.options = options - def _draw_month(self, cr, rect, month, year, daycell_thres): + def _draw_month(self, cr, rect, month, year): """this method renders a calendar month, it B{should be overridden} in any subclass @param cr: cairo context @param rect: rendering rect @param month: month @param year: year - @param daycell_thres: short/long day cell threshold """ raise NotImplementedError("base _draw_month() should be overridden") @@ -288,7 +287,7 @@ class CalendarRenderer(object): for (m,y) in p: k = len(p) - num_placed - 1 if z_order == "decreasing" else num_placed self._draw_month(page.cr, grid.item_seq(k, self.options.grid_order == "column"), - month=m, year=y, daycell_thres = self.options.short_daycell_ratio) + month=m, year=y) num_placed += 1 if (y > yy[-1]): yy.append(y) if not self.options.month_with_year and not self.options.no_footer: diff --git a/layouts/bars.py b/layouts/bars.py index c8fe838..fca35b7 100644 --- a/layouts/bars.py +++ b/layouts/bars.py @@ -30,8 +30,7 @@ parser = _base.get_parser(__name__) class CalendarRenderer(_base.CalendarRenderer): """bars layout class""" - #default thres = 2.5 - def _draw_month(self, cr, rect, month, year, daycell_thres): + def _draw_month(self, cr, rect, month, year): S,G,L = self.Theme make_sloppy_rect(cr, rect, G.month.sloppy_dx, G.month.sloppy_dy, G.month.sloppy_rot) @@ -58,7 +57,7 @@ class CalendarRenderer(_base.CalendarRenderer): day_style = holiday_tuple[2] dcell = _base.DayCell(day = (dom, day), header = holiday_tuple[0], footer = holiday_tuple[1], theme = (day_style, G.dom, L), show_day_name = True) - dcell.draw(cr, R, daycell_thres) + dcell.draw(cr, R, self.options.short_daycell_ratio) else: day_style = S.dom draw_box(cr, rect = R, stroke_rgba = day_style.frame, fill_rgba = day_style.bg, diff --git a/layouts/classic.py b/layouts/classic.py index dba88e1..cc5e71a 100644 --- a/layouts/classic.py +++ b/layouts/classic.py @@ -37,8 +37,7 @@ def _weekrows_of_month(year, month): class CalendarRenderer(_base.CalendarRenderer): """classic tiles layout class""" - #default thres = 2.5 - def _draw_month(self, cr, rect, month, year, daycell_thres): + def _draw_month(self, cr, rect, month, year): S,G,L = self.Theme make_sloppy_rect(cr, rect, G.month.sloppy_dx, G.month.sloppy_dy, G.month.sloppy_rot) @@ -81,7 +80,7 @@ class CalendarRenderer(_base.CalendarRenderer): day_style = holiday_tuple[2] dcell = _base.DayCell(day = (dom, col), header = holiday_tuple[0], footer = holiday_tuple[1], theme = (day_style, G.dom, L), show_day_name = False) - dcell.draw(cr, R, daycell_thres) + dcell.draw(cr, R, self.options.short_daycell_ratio) else: day_style = S.dom_weekend if col >= 5 else S.dom draw_box(cr, rect = R, stroke_rgba = day_style.frame, fill_rgba = day_style.bg, diff --git a/layouts/sparse.py b/layouts/sparse.py new file mode 100644 index 0000000..8f81e36 --- /dev/null +++ b/layouts/sparse.py @@ -0,0 +1,185 @@ +# -*- coding: utf-8 -*- +# callirhoe - high quality calendar rendering +# Copyright (C) 2012 George M. Tzoumas + +# Sparse Layout Module +# Copyright (C) 2013 Neels Hofmeyr + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see http://www.gnu.org/licenses/ + +"""sparse layout""" + +from lib.xcairo import * +from lib.geom import * +import calendar +import optparse +import sys +from datetime import date, timedelta + +import _base + +def get_parser(layout_name): + """get the parser object for the layout command-line arguments + + @param layout_name: corresponding python module (.py file) + """ + lname = layout_name.split(".")[1] + parser = optparse.OptionParser(usage="%prog (...) --layout " + lname + " [options] (...)",add_help_option=False) + parser.add_option("--rows", type="int", default=1, help="force grid rows [%default]") + parser.add_option("--cols", type="int", default=0, + help="force grid columns [%default]; if ROWS and COLS are both non-zero, " + "calendar will span multiple pages as needed; if one value is zero, it " + "will be computed automatically in order to fill exactly 1 page") + parser.add_option("--grid-order", choices=["row","column"],default="row", + help="either `row' or `column' to set grid placing order row-wise or column-wise [%default]") + parser.add_option("--z-order", choices=["auto", "increasing", "decreasing"], default="auto", + help="either `increasing' or `decreasing' to set whether next month (in grid order) " + "lies above or below the previously drawn month; this affects shadow casting, " + "since rendering is always performed in increasing z-order; specifying `auto' " + "selects increasing order if and only if sloppy boxes are enabled [%default]") + parser.add_option("--month-with-year", action="store_true", default=False, + help="displays year together with month name, e.g. January 1980; suppresses year from footer line") + parser.add_option("--no-footer", action="store_true", default=False, + help="disable footer line (with year and rendered-by message)") + parser.add_option("--symmetric", action="store_true", default=False, + help="force symmetric mode (equivalent to --geom-var=month.symmetric=1). " + "In symmetric mode, day cells are equally sized and all month boxes contain " + "the same number of (possibly empty) cells, independently of how many days or " + "weeks per month. In asymmetric mode, empty rows are eliminated, by slightly " + "resizing day cells, in order to have uniform month boxes.") + parser.add_option("--padding", type="float", default=None, + help="set month box padding (equivalent to --geom-var=month.padding=PADDING)") + parser.add_option("--no-shadow", action="store_true", default=None, + help="disable box shadows") + parser.add_option("--opaque", action="store_true", default=False, + help="make background opaque (white fill)") + parser.add_option("--swap-colors", action="store_true", default=None, + help="swap month colors for even/odd years") + return parser + +parser = get_parser(__name__) + +def _draw_day_cell(cr, rect, day, header, footer, theme, show_day_name, text_height=None): + ds,G,L = theme + year, month, day_of_month, day_of_week = day + draw_box(cr, rect, ds.bg, ds.bg, mm_to_dots(ds.frame_thickness)) + + if day_of_month > 1: + x, y, w, h = rect + draw_line(cr, (x, y, w, 0), ds.frame, mm_to_dots(ds.frame_thickness)) + + if (text_height is not None) and (text_height > 0): + x, y, w, h = rect + h_diff = h - text_height + if h_diff > 0: + y += h_diff / 2 + h = text_height + rect = (x, y, w, h) + + x, y, w, h = rect + ww = h + Rleft = (x + 0.1 * h, y + 0.2 * h, ww - 0.2 * h, .6 * h) + Rmiddle = (x + h, y, ww, h) + Rmiddle_top = (x + h + 0.1 * h, y + 0.2 * h, ww, .18 * h) + bottom_h = .8 * h + Rmiddle_bottom = (x + h + 0.1 * h, y + h - bottom_h, ww, bottom_h - 0.2 * h) + #Rmiddle_top = rect_rel_scale(Rmiddle_top, .8, 0.6) + #Rmiddle_bottom = rect_rel_scale(Rmiddle, .8, 0.6) + Rright_header = (x + 2*h, y + 0.1 * h, w - 2 * ww - 0.2 * ww, 0.28 * h) + Rright_footer = (x + 2*h, y + 0.6 * h, w - 2 * ww - 0.2 * ww, 0.28 * h) + x, y, w, h = Rmiddle_bottom + hh = h + h = float(h) * 0.6 + y += float(hh) - h + Rmiddle_bottom = (x, y, w, h) + valign = 0 if show_day_name else 2 + # draw day of month (number) + draw_str(cr, text = str(day_of_month), rect = Rleft, scaling = -1, stroke_rgba = ds.fg, + align = (1,valign), font = ds.font, measure = "88") + # draw name of day + if show_day_name: + draw_str(cr, text = L.day_name[day_of_week], rect = Rmiddle_bottom, + scaling = -1, stroke_rgba = ds.fg, align = (0,valign), + font = ds.font, measure = "Mo") + # week number + if day_of_week == 0 or (day_of_month == 1 and month == 1): + week_nr = date(year, month, day_of_month).isocalendar()[1] + draw_str(cr, text = "%s%d" % (L.week_of_year_prefix, week_nr), rect = Rmiddle_top, + scaling = -1, stroke_rgba = ds.fg, align = (0,valign), + font = ds.header_font, measure = "W88") + + if header: + draw_str(cr, text = header, rect = Rright_header, scaling = -1, + stroke_rgba = ds.header, align = (1,1), font = ds.header_font, + measure='MgMgMgMgMgMgMgMgMg') + if footer: + draw_str(cr, text = footer, rect = Rright_footer, scaling = -1, + stroke_rgba = ds.footer, align = (1,1), font = ds.header_font, + measure='MgMgMgMgMgMgMgMgMg') + +class CalendarRenderer(_base.CalendarRenderer): + """sparse layout class""" + def _draw_month(self, cr, rect, month, year): + S,G,L = self.Theme + make_sloppy_rect(cr, rect, G.month.sloppy_dx, G.month.sloppy_dy, G.month.sloppy_rot) + + day, span = calendar.monthrange(year, month) + wmeasure = 'A'*max(map(len,L.day_name)) + mmeasure = 'A'*max(map(len,L.month_name)) + + rows = 31 if G.month.symmetric else span + grid = VLayout(rect_from_origin(rect), 32) # title bar always symmetric + dom_grid = VLayout(grid.item_span(31,1), rows) + + # determine text height + tmp_grid = VLayout(grid.item_span(31,1), 31) + text_height = tmp_grid.item(0)[3] + + # draw box shadow + if S.month.box_shadow: + f = S.month.box_shadow_size + shad = (f,-f) if G.landscape else (f,f) + draw_shadow(cr, rect_from_origin(rect), shad) + + # draw day cells + for dom in range(1,span+1): + R = dom_grid.item(dom-1) + holiday_tuple = self.holiday_provider(year, month, dom, day) + day_style = holiday_tuple[2] + header = holiday_tuple[0] + footer = holiday_tuple[1] + if footer: + if header: + header = "%s, %s" % (header, footer) + else: + header = footer + _draw_day_cell(cr, rect = R, day = (year, month, dom, day), + header = header, footer = None, + theme = (day_style, G.dom, L), show_day_name = True, + text_height = text_height) + + day = (day + 1) % 7 + + # draw month title (name) + mcolor = S.month.color_map_bg[year%2][month] + mcolor_fg = S.month.color_map_fg[year%2][month] + R_mb = grid.item(0) + R_text = rect_rel_scale(R_mb, 1, 0.5) + mshad = None + if S.month.text_shadow: + f = S.month.text_shadow_size + mshad = (f,-f) if G.landscape else (f,f) + draw_str(cr, text = L.month_name[month], rect = R_text, scaling = -1, stroke_rgba = mcolor_fg, + align = (2,0), font = S.month.font, measure = mmeasure, shadow = mshad) + cr.restore() diff --git a/style/bw.py b/style/bw.py index db61a82..5a4a8b2 100644 --- a/style/bw.py +++ b/style/bw.py @@ -16,7 +16,7 @@ # --- style.bw --- -"""module defining the black-& white style""" +"""module defining the black & white style""" class dow: """day of week style""" diff --git a/style/bw_sparse.py b/style/bw_sparse.py new file mode 100644 index 0000000..5456754 --- /dev/null +++ b/style/bw_sparse.py @@ -0,0 +1,75 @@ +# callirhoe - high quality calendar rendering +# Copyright (C) 2012 George M. Tzoumas + +# Sparse Style Definition +# Copyright (C) 2013 Neels Hofmeyr + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see http://www.gnu.org/licenses/ + +# --- style.bw --- + +"""module defining the black & white sparse style + +to be used with sparse layout +""" + +class dow: + """day of week style""" + fg = (0,0,0) + frame_thickness = 0.1 + frame = (.5, .5, .5) + font = "Arial" + +class dom: + """day of month style""" + bg = (1,1,1) + frame = (.9, .9, .9) + frame_thickness = 0.1 + fg = (0.3,0.3,0.3) + font = "Times New Roman" + header = (0.3,0.3,0.3) + footer = header + header_font = footer_font = "Arial" + +class dom_holiday(dom): + """day of month (holiday, indicated by the OFF flag in the holiday file)""" + bg = (0.95,0.95,0.95) + +class dom_weekend(dom_holiday): + """day of month style (weekend)""" + font = ("Times New Roman", 0, 1) + +class dom_weekend_holiday(dom_weekend): + """day of month (weekend & holiday)""" + pass + +class dom_multi(dom_holiday): + """day of month (multi-day holiday)""" + pass + +class dom_weekend_multi(dom_weekend_holiday): + """day of month (weekend in multi-day holiday)""" + pass + +class month: + """month style""" + font = ("Times New Roman", 0, 1) + frame = (0,0,0) + frame_thickness = 0.2 + bg = (1,1,1) + color_map = ((1,1,1),)*13 + color_map_bg = (((1,1,1),)*13,((.8,.8,.8),)*13) + color_map_fg = (((0,0,0),)*13,((0,0,0),)*13) + box_shadow = False + text_shadow = False