callirhoe/callirhoe.py
2014-10-30 21:55:10 +00:00

405 lines
16 KiB
Python
Executable File

#!/usr/bin/env python2.7
# -*- 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/
"""high quality calendar rendering"""
# TODO:
# fix auto-measure rendering (cairo)
# fix plugin loading (without global vars)
# week markers selectable
# test layouts
# allow to change background color (fill), other than white
# page spec parse errors
# mobile themes (e.g. 800x480)
# photo support (like ImageMagick's polaroid effect)
# .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
# styles and geometries could be merged, css-like
# then we can apply a chain of --style a --style b ...
# and b inherits from a and so on
# however, this would require dynamically creating a class that inherits from others...
# CANNOT UPGRADE TO argparse !!! -- how to handle [[month] year] form?
_version = "0.4.0"
_copyright = """Copyright (C) 2012-2014 George M. Tzoumas
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law."""
import calendar
import sys
import time
import optparse
import lib.xcairo as xcairo
import lib.holiday as holiday
class Abort(Exception):
pass
from lib.plugin import *
# TODO: SEE IF IT CAN BE MOVED INTO lib.plugin ...
def import_plugin(plugin_path, 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
@rtype: module
@note: Aimed for internal use with I{lang}, I{style}, I{geom}, I{layouts}.
"""
try:
found = available_files(plugin_path, cat, preset)
if len(found) == 0: raise IOError
m = __import__("%s.%s" % (cat,preset), globals(), locals(), [ "*" ])
return m
except IOError:
sys.exit("callirhoe: %s definition '%s' not found, use %s to see available definitions" % (longcat,
preset,listopt))
except ImportError:
sys.exit("callirhoe: error loading %s definition '%s'" % (longcat, preset))
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) (for printing):
$ callirhoe --landscape my_calendar.pdf
Landscape via rotation (for screen):
$ callirhoe --paper=a4w --rows=3 my_calendar.pdf
Let's try with bars instead of boxes:
$ callirhoe -t bars my_calendar.pdf
In landscape mode, one row only looks quite good:
$ callirhoe -t bars --landscape --rows=1 my_calendar.pdf
How about a more flat look?
$ callirhoe -t sparse -s bw_sparse --rows=1 --cols=3 my_calendar.pdf
Calendar of 24 consecutive months, starting from current month:
$ callirhoe 0:24 0 my_calendar.pdf
Create a 600-dpi PNG file so that we can edit it with some effects in order to print an A3 poster:
$ callirhoe my_poster.png --paper=a3 --dpi=600 --opaque
Create a calendar as a full-hd wallpaper (1920x1080):
$ callirhoe wallpaper.png --paper=-1920:-1080 --opaque --rows=3 --no-shadow -s rainbow-gfs
and do some magic with ImageMagick! ;)
$ convert wallpaper.png -negate fancy.png
"""
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)
def atoi(s, lower_bound=None, upper_bound=None, prefix=''):
"""convert string to integer, exiting on error (for cmdline parsing)
@param lower_bound: perform additional check so that value >= I{lower_bound}
@param upper_bound: perform additional check so that value <= I{upper_bound}
@param prefix: output prefix for error reporting
@rtype: int
"""
try:
k = int(s);
if lower_bound is not None:
if k < lower_bound:
raise Abort(prefix + "value '" + s +"' out of range: should not be less than %d" % lower_bound)
if upper_bound is not None:
if k > upper_bound:
raise Abort(prefix + "value '" + s +"' out of range: should not be greater than %d" % upper_bound)
except ValueError as e:
raise Abort(prefix + "invalid integer value '" + s +"'")
return k
def _parse_month(mstr):
"""get a month value (0-12) from I{mstr}, exiting on error (for cmdline parsing)
@rtype: int
"""
m = atoi(mstr,lower_bound=0,upper_bound=12,prefix='month: ')
if m == 0: m = time.localtime()[1]
return m
def parse_month_range(s):
"""return (Month,Span) by parsing range I{Month}, I{Month1}-I{Month2} or I{Month}:I{Span}
@rtype: (int,int)
"""
if ':' in s:
t = s.split(':')
if len(t) != 2: raise Abort("invalid month range '" + s + "'")
Month = _parse_month(t[0])
MonthSpan = atoi(t[1],lower_bound=0,prefix='month span: ')
elif '-' in s:
t = s.split('-')
if len(t) != 2: raise Abort("invalid month range '" + s + "'")
Month = _parse_month(t[0])
MonthSpan = atoi(t[1],lower_bound=Month+1,prefix='month range: ') - Month + 1
else:
Month = _parse_month(s)
MonthSpan = 1
return (Month,MonthSpan)
def parse_year(ystr):
"""get a year value (>=0) from I{ystr}, exiting on error (for cmdline parsing)
@rtype: int
"""
y = atoi(ystr,lower_bound=0,prefix='year: ')
if y == 0: y = time.localtime()[0]
return y
def extract_parser_args(arglist, parser, pos = -1):
"""extract options belonging to I{parser} along with I{pos} positional arguments
@param arglist: argument list to extract
@param parser: parser object to be used for extracting
@param pos: number of positional options to be extracted
if I{pos}<0 then all positional arguments are extracted, otherwise,
only I{pos} arguments are extracted. arglist[0] (usually sys.argv[0]) is also positional
argument!
@rtype: ([str,...],[str,...])
@return: tuple (argv1,argv2) with extracted argument list and remaining argument list
"""
argv = [[],[]]
posc = 0
push_value = None
for x in arglist:
if push_value:
push_value.append(x)
push_value = None
continue
# get option name (long options stop at '=')
y = x[0:x.find('=')] if '=' in x else x
if x[0] == '-':
if parser.has_option(y):
argv[0].append(x)
if not x.startswith('--') and parser.get_option(y).takes_value():
push_value = argv[0]
else:
argv[1].append(x)
else:
if pos < 0:
argv[0].append(x)
else:
argv[posc >= pos].append(x)
posc += 1
return tuple(argv)
def get_parser():
"""get the argument parser object
@rtype: optparse.OptionParser
"""
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 + '\n' + _copyright)
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")
return parser
def main_program():
parser = get_parser()
sys.argv,argv2 = extract_parser_args(sys.argv,parser)
(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: return
plugin_path = "/"
Language = import_plugin(plugin_path, "lang", "language", "languages", "--list-languages", options.lang)
Style = import_plugin(plugin_path, "style", "style", "styles", "--list-styles", options.style)
Geometry = import_plugin(plugin_path, "geom", "geometry", "geometries", "--list-geometries", options.geom)
Layout = import_plugin(plugin_path, "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()
return
if options.examples:
print_examples()
return
# 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()
return
#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:
Month, MonthSpan = parse_month_range(args[0])
Year = parse_year(args[1])
Outfile = args[2]
if MonthSpan == 0:
raise Abort("callirhoe: empty calendar requested, aborting")
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:
Language.day_name = Language.short_day_name
if options.short_monthnames:
Language.month_name = Language.short_month_name
else:
Language.month_name = Language.long_month_name
renderer = Layout.CalendarRenderer(Outfile, Year, Month, MonthSpan,
(Style,Geometry,Language), hprovider, _version, loptions)
renderer.render()
if __name__ == "__main__":
try:
# import pkg_resources
# print pkg_resources.resource_listdir(__name__,"/")
main_program()
except Abort as e:
sys.exit(e.args[0])