0.3.0 release

holiday support (with Orthodox & Catholic Easter based holidays)
fully documented code
code refactoring, better layout separation
sparse layout and minor patches {Neels}
more languages: german, turkish



git-svn-id: https://callirhoe.googlecode.com/svn/trunk@51 81c8bb96-aa45-f2e2-0eef-c4fa4a15c6df
This commit is contained in:
geortz@gmail.com 2014-09-04 15:13:27 +00:00
commit ab38ed51f6
27 changed files with 1999 additions and 548 deletions

View File

@ -2,7 +2,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
@ -17,17 +17,24 @@
# 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:
# 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
# implement DATA SOURCES
# python source documentation
# photo support (like ImageMagick's polaroid effect)
# .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,19 +45,35 @@
# 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 ...
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]
@ -65,44 +88,19 @@ 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("-H", "--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]")
def print_examples():
"""print usage examples"""
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
@ -120,95 +118,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 (-H) to see available options" % x)
(Layout.options,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.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):
"""convert integer to string, exiting on error (for cmdline parsing)"""
try:
k = int(s);
except ValueError as e:
@ -216,46 +134,192 @@ 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]")
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)
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, options.multiday_holidays)
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
Layout.draw_calendar(Outfile, Year, Month, MonthSpan, (Style,Geometry), _version)
renderer = Layout.CalendarRenderer(Outfile, Year, Month, MonthSpan,
(Style,Geometry,Language), hprovider, _version, loptions)
renderer.render()

View File

@ -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

View File

@ -16,6 +16,8 @@
# --- geom.sloppy ---
"""module defining the sloppy geometry"""
import default
class dom(default.dom): pass

View File

@ -0,0 +1,33 @@
# 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|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
ce|-2||Good Friday|
ce|0||Easter|off
ce|1||Easter Monday|off
ce|39||Ascension|off
ce|50||Whit Monday|off
d|20130223-20130310|winter vacations (B)||multi
d|20140222-20140309|winter vacations (B)||multi

View File

@ -0,0 +1,33 @@
# 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||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
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
d|20130223-20130310|vacances d'hiver (B)||multi
d|20140222-20140309|vacances d'hiver (B)||multi

View File

@ -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

View File

@ -0,0 +1,39 @@
# 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||ΠΡΩΤΟΧΡΟΝΙΑ|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
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|Καθολικό Πάσχα||

View File

@ -0,0 +1,70 @@
# 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|Βασίλης||
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|Στέφανος||
oe|-43|Αγίων Θεοδώρων||
oe|0|Αναστάσιος, Πασχάλης, Λάμπρος||
oe|-8|Λάζαρος||
oe|-7|Βάιος||
oe|7|Θωμάς||
oe|56|Αγίων Πάντων||

36
lang/DE.py Normal file
View File

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
# callirhoe - high quality calendar rendering
# Copyright (C) 2012 George M. Tzoumas
""" German language definition file"""
# 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/
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' ]
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' ]
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'

View File

@ -16,14 +16,18 @@
# 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'Τετάρτη',
"""Greek language definition file"""
long_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'Απρίλιος',
long_month_name = [ '', u'Ιανουάριος', u'Φεβρουάριος', u'Μάρτιος', u'Απρίλιος',
u'Μάιος', u'Ιούνιος', u'Ιούλιος', u'Αύγουστος',
u'Σεπτέμβριος', u'Οκτώβριος', u'Νοέμβριος', u'Δεκέμβριος' ]
short_month_name = [ '', u'Ιαν', u'Φεβ', u'Μαρ', u'Απρ', u'Μαϊ', u'Ιον', u'Ιολ',
u'Αυγ', u'Σεπ', u'Οκτ', u'Νοε', u'Δεκ' ]
week_of_year_prefix = u'Ε'

View File

@ -16,12 +16,14 @@
# 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',
u'Thursday', u'Friday', u'Saturday', u'Sunday' ]
"""English language definition file"""
short_day_name = [ u'Mon', u'Tue', u'Wed', u'Thu', u'Fri', u'Sat', u'Sun' ]
long_day_name = [ u'Monday', u'Tuesday', u'Wednesday',
u'Thursday', u'Friday', u'Saturday', u'Sunday' ]
month_name = [ '',
short_day_name = [ u'Mo', u'Tu', u'We', u'Th', u'Fr', u'Sa', u'Su' ]
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 +31,5 @@ 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' ]
week_of_year_prefix = u'W'

View File

@ -16,15 +16,19 @@
# 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',
"""French language definition file"""
long_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 = [ '',
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' ]
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'

33
lang/TR.py Normal file
View File

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
# callirhoe - high quality calendar rendering
# Copyright (C) 2012 George M. Tzoumas
""" Turkish language definition file"""
# 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/
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' ]
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' ]
short_month_name = long_month_name
week_of_year_prefix = u'H'

304
layouts/_base.py Normal file
View File

@ -0,0 +1,304 @@
# -*- 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/
"""base layout module -- others may inherit from this one"""
import optparse
from lib.xcairo import *
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]")
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("--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):
"""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 class,Geometry class,Language module) 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
self.footer = footer
self.theme = theme
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
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, 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, 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, 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, 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
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, 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, 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, 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, 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:
self._draw_long(cr, rect)
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 module,Geometry module,Language module) 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
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):
"""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
"""
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):
"""main calendar rendering routine"""
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.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)
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)
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), 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), scaling = -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()

85
layouts/bars.py Normal file
View File

@ -0,0 +1,85 @@
# -*- 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/
"""bars layout"""
from lib.xcairo import *
from lib.geom import *
import calendar
import optparse
import sys
from datetime import date, timedelta
import _base
parser = _base.get_parser(__name__)
class CalendarRenderer(_base.CalendarRenderer):
"""bars 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)
mmeasure = 'A'*max(map(len,L.month_name))
if self.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 = self.holiday_provider(year, month, dom, day)
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, 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,
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, scaling = -1, stroke_rgba = mcolor_fg,
align = (2,0), font = S.month.font, measure = mmeasure, shadow = mshad)
cr.restore()

View File

@ -1,6 +1,6 @@
# -*- 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
@ -15,357 +15,93 @@
# 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 *
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("--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("--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,
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 weekrows_of_month(year, month):
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
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 = 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 = calendar.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)
# 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):
"""classic tiles 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)
def _draw_day_cell_long(cr, rect, day, header, footer, theme, show_day_name):
S,G = 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 = calendar.day_name[day_of_week][0], 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
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)
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)
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_matrix(cr, rect, month, year, theme, 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)
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))
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 = calendar.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):
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,
short_thres = daycell_thres)
else:
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, scaling = -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 = 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()
# 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, 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,
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, scaling = -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, 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)
wmeasure = 'A'*max(map(len,calendar.day_name))
mmeasure = 'A'*max(map(len,calendar.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)
# 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):
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,
theme = (day_style, G.dom), show_day_name = True, short_thres = daycell_thres)
else:
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)
draw_str(cr, text = calendar.month_name[month], 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, bar_thres = 0.7, daycell_thres = 2.5):
if rect_ratio(rect) >= bar_thres:
draw_month_matrix(cr, rect, month, year, theme, daycell_thres)
else:
draw_month_bar(cr, rect, month, year, theme, daycell_thres)
#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, version_string):
S,G = 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 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,
bar_thres = options.month_bar_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:
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()

185
layouts/sparse.py Normal file
View File

@ -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()

View File

@ -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]))
@ -113,32 +170,50 @@ class HLayout(VLayout): # transpose of 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
@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))

429
lib/holiday.py Normal file
View File

@ -0,0 +1,429 @@
# -*- 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)
def _strip_empty(sl):
"""strip empty strings from list I{sl}"""
return filter(lambda z: z, sl) if sl else []
def _flatten(sl):
"""join list I{sl} into a comma-separated string"""
if not sl: return None
return ', '.join(sl)
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)
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
REMINDER = 4
def __init__(self, header = [], footer = [], flags_str = None):
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"""
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 a comma-separated string for L{header_list}"""
return _flatten(self.header_list)
def footer(self):
"""return a comma-separated string for L{footer_list}"""
return _flatten(self.footer_list)
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
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 _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:
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):
"""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()
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
self.s_multi = s_multi
self.s_weekend_multi = s_weekend_multi
self.multiday_markers = multiday_markers
def _parse_day_record(self, fields):
"""return tuple (etype,ddef,footer,header,flags)
@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))
for i in range(len(fields)):
if len(fields[i]) == 0: fields[i] = None
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, 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.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.multiday_markers:
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),
range(4)))
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)
hol = Holiday([header], [footer], flags)
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(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)
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
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 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:
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)
@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:
return (hol.header(),hol.footer(),self.get_style(hol.flags,dow))
else:
return (None,None,self.get_style(0,dow))
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)
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)

View File

@ -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
@ -18,7 +18,7 @@
# *****************************************
# #
# plugin handling routines #
""" plugin handling routines """
# #
# *****************************************
@ -26,20 +26,33 @@ 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"
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))
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
@ -47,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 "." ]

View File

@ -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
@ -30,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(':')
@ -49,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:
@ -68,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
@ -91,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:
@ -119,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:
@ -132,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)
@ -151,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,7 +256,34 @@ 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):
"""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)
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):
"""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
@ -189,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
@ -202,13 +339,17 @@ 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]
ratio, tratio = w*1.0/h, mw*1.0/mh;
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
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]

View File

@ -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,19 +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_style(dom_holiday):
bg = (0,0,0)
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

75
style/bw_sparse.py Normal file
View File

@ -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

View File

@ -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,17 +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)
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_style(dom_holiday):
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

View File

@ -14,12 +14,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/
# --- style.gfs (Greek Font Society fonts) ---
# --- style.gfs ---
# day of week
"""module defining Greek Font Society fonts for default style"""
import default
# day of week
class dow(default.dow):
font = "GFS Neohellenic"
@ -36,7 +37,15 @@ 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"
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"

View File

@ -16,10 +16,11 @@
# --- style.rainbow ---
# day of week
"""module defining rainbow color & gfs style"""
import gfs
# day of week
class dow(gfs.dow): pass
# day of month
@ -29,7 +30,11 @@ 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
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

View File

@ -16,10 +16,11 @@
# --- style.rainbow ---
# day of week
"""module defining rainbow color style"""
import default
# day of week
class dow(default.dow): pass
# day of month
@ -29,7 +30,11 @@ 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
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