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