diff --git a/AUTHORS b/AUTHORS index fcc5e4e..877355e 100644 --- a/AUTHORS +++ b/AUTHORS @@ -12,7 +12,7 @@ George M. Tzoumas CONTRIBUTORS ------------ Neels Hofmeyr - +Nick Kavalieris LANGUAGE DEFINITIONS -------------------- diff --git a/ChangeLog b/ChangeLog index b9b0846..5a56141 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,10 @@ +******************************* +* Version 0.4.1 (Nov 2014) * +******************************* ++ packaging support for Python/Arch Linux {Nick} +! calmagick: fixed bug regarding --min-size being greater than --max-size +* refactored some common code between callirhoe and calmagick + ******************************* * Version 0.4.0 (Oct 2014) * ******************************* diff --git a/INSTALL b/INSTALL index 581ece7..0aa5926 100644 --- a/INSTALL +++ b/INSTALL @@ -6,6 +6,12 @@ QUICK INSTALLATION GUIDE +CONTENTS + 1) FROM COMPRESSED ARCHIVE + 2) FROM SVN + 3) INSTALLING INTO BINARY PATH + 4) INSTALLATION FOR ARCH LINUX + (rough installation guide for novice users...) 1) FROM COMPRESSED ARCHIVE @@ -13,8 +19,9 @@ QUICK INSTALLATION GUIDE Download the latest version from the project's page, or directly from: https://callirhoe.googlecode.com/svn/wiki/releases/ - -You end up with a file named callirhoe-X.Y.Z.7z where X.Y.Z the version number (for example 0.4.0). + +You end up with a file named callirhoe-X.Y.Z.7z where X.Y.Z the version +number (for example 0.4.0). Extract the contents of the archive $ 7z x callirhoe-X.Y.Z.7z @@ -26,13 +33,8 @@ Now you can launch the program, e.g. $ ./callirhoe.py foo.pdf -You may want to add a link to your path, $HOME/bin or /usr/local/bin: - -$ ln -s `pwd`/callirhoe.py $HOME/bin/callirhoe - -You can do the same with calmagick.py. You may also install it system-wide, -for example in /opt. In this case, keep in mind, that ~/.callirhoe/ is also -searched for additional definitions, styles etc. +See section 3 for how to install callirhoe so that it lies in your +executable path. 2) FROM SVN @@ -51,3 +53,53 @@ $ svn up You can launch the program as usual: $ ./callirhoe.py foo.pdf + +3) INSTALLING INTO BINARY PATH + +You can add a link to your path, $HOME/bin or /usr/local/bin: + +$ ln -s `pwd`/callirhoe.py $HOME/bin/callirhoe + +You can do the same with calmagick.py. You may also install it +system-wide, for example in /opt. In this case, keep in mind, that +~/.callirhoe/ is also searched for additional definitions, styles etc. + +If you do not plan to mess with the source, you may create a binary +python package. This is not exactly a binary, it is a zip archive +containing compiled python bytecode, which is quite compact. To do so, +simply run: + +$ make + +This will create two executables, 'callirhoe' and 'calmagick'. Now you +can install them into your binary path as follows: + +$ make install + +this will typically install to /usr/local/bin (and the holiday files into +/usr/local/share/callirhoe/holidays). You can specify another prefix: + +$ make install DESTDIR=/my/other/dir + +Now you can remove the source dir, as it is no longer needed. + +4) INSTALLATION FOR ARCH LINUX + +There is a PKGBUILD file you can use to install. Normally you get just +the PKGBUILD from the webpage from AUR (). It is also +included in the source distribution, but this is a bit redundant (see +below). + +Place the PKGBUILD into a directory and run: + +$ makepkg -si + +( -s will automatically install missing depedencies; also note that this +will redownload the source from the svn ) + +Arch will do the rest for you. + +In the unlikely event that you don't have "makepkg" already installed +you can find information about it's installation here: + +https://wiki.archlinux.org/index.php/Arch_User_Repository diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..635a8d1 --- /dev/null +++ b/Makefile @@ -0,0 +1,16 @@ +DESTDIR=/usr/local + +all: + cd scripts && ./make_pkg + +install: install-package + +install-package: + mkdir -p $(DESTDIR)/bin + mkdir -p $(DESTDIR)/share/callirhoe/holidays + install -m755 callirhoe $(DESTDIR)/bin/callirhoe + install -m755 calmagick $(DESTDIR)/bin/calmagick + install -m644 holidays/* $(DESTDIR)/share/callirhoe/holidays/ + +clean: + rm -f callirhoe calmagick diff --git a/callirhoe.py b/callirhoe.py index 09d051a..94aabb2 100755 --- a/callirhoe.py +++ b/callirhoe.py @@ -44,21 +44,13 @@ # 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 -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 +import lib from lib.plugin import * # TODO: SEE IF IT CAN BE MOVED INTO lib.plugin ... @@ -80,12 +72,16 @@ def import_plugin(plugin_paths, cat, longcat, longcat2, listopt, preset): @note: Aimed for internal use with I{lang}, I{style}, I{geom}, I{layouts}. """ try: - found = available_files(plugin_paths[0], cat, preset) + available_files(plugin_paths[1], cat, preset) + found = [] + for path in plugin_paths: + found += available_files(path, cat, preset) if len(found) == 0: raise IOError - old = sys.path[0]; - sys.path[0] = found[0][1] - m = __import__("%s.%s" % (cat,preset), globals(), locals(), [ "*" ]) - sys.path[0] = old + if found[0][1] == "resource:": + m = __import__("%s.%s" % (cat,preset), globals(), locals(), [ "*" ]) + else: + sys.path.insert(0, found[0][1]) + m = __import__("%s.%s" % (cat,preset), globals(), locals(), [ "*" ]) + sys.path.pop(0) return m except IOError: sys.exit("callirhoe: %s definition '%s' not found, use %s to see available definitions" % (longcat, @@ -136,103 +132,6 @@ def add_list_option(parser, opt): 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 @@ -243,7 +142,7 @@ def get_parser(): "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) + version="callirhoe " + lib._version + '\n' + lib._copyright) parser.add_option("-l", "--lang", dest="lang", default="EN", help="choose language [%default]") parser.add_option("-t", "--layout", dest="layout", default="classic", @@ -289,7 +188,7 @@ def get_parser(): def main_program(): parser = get_parser() - sys.argv,argv2 = extract_parser_args(sys.argv,parser) + sys.argv,argv2 = lib.extract_parser_args(sys.argv,parser) (options,args) = parser.parse_args() list_and_exit = False @@ -360,16 +259,16 @@ def main_program(): Month, MonthSpan = 1, 12 Outfile = args[0] elif len(args) == 2: - Year = parse_year(args[0]) + Year = lib.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]) + Month, MonthSpan = lib.parse_month_range(args[0]) + Year = lib.parse_year(args[1]) Outfile = args[2] if MonthSpan == 0: - raise Abort("callirhoe: empty calendar requested, aborting") + raise lib.Abort("callirhoe: empty calendar requested, aborting") Geometry.landscape = options.landscape xcairo.XDPI = options.dpi @@ -395,11 +294,11 @@ def main_program(): Language.month_name = Language.long_month_name renderer = Layout.CalendarRenderer(Outfile, Year, Month, MonthSpan, - (Style,Geometry,Language), hprovider, _version, loptions) + (Style,Geometry,Language), hprovider, lib._version, loptions) renderer.render() if __name__ == "__main__": try: main_program() - except Abort as e: + except lib.Abort as e: sys.exit(e.args[0]) diff --git a/calmagick.py b/calmagick.py index 38d1485..db478b4 100755 --- a/calmagick.py +++ b/calmagick.py @@ -34,7 +34,7 @@ import optparse import Queue import threading -from callirhoe import extract_parser_args, parse_month_range, parse_year, atoi, Abort, _version, _copyright +import lib from lib.geom import rect_rel_scale # MAYBE-TODO @@ -198,7 +198,7 @@ def get_parser(): If IMAGE is a single file, then a calendar of the current month is overlayed. If IMAGE contains wildcards, then every month is generated according to the --range option, advancing one month for every photo file. Photos will be reused in a round-robin fashion if more calendar -months are requested.""", version="callirhoe.CalMagick " + _version + '\n' + _copyright) +months are requested.""", version="callirhoe.CalMagick " + lib._version + '\n' + lib._copyright) parser.add_option("--outdir", default=".", help="set directory for the output image(s); directory will be created if it does not already exist [%default]") parser.add_option("--outfile", default=None, @@ -298,13 +298,15 @@ photos.""") def check_parsed_options(options): """set (remaining) default values and check validity of various option combinations""" if options.min_size is None: - options.min_size = 0.333 if options.placement in ['min','max','random'] else 0.05 + options.min_size = min(0.333,options.max_size) if options.placement in ['min','max','random'] else min(0.05,options.max_size) + if options.min_size > options.max_size: + raise lib.Abort("calmagick: --min-size should not be greater than --max-size") if options.sample is not None and not options.range: - raise Abort("calmagick: --sample requested without --range") + raise lib.Abort("calmagick: --sample requested without --range") if options.outfile is not None and options.range: - raise Abort("calmagick: you cannot specify both --outfile and --range options") + raise lib.Abort("calmagick: you cannot specify both --outfile and --range options") if options.sample is not None and options.shuffle: - raise Abort("calmagick: you cannot specify both --shuffle and --sample options") + raise lib.Abort("calmagick: you cannot specify both --shuffle and --sample options") if options.shuffle: options.sample = 0 if options.sample is None: @@ -370,7 +372,7 @@ def get_outfile(infile, outdir, base_prefix, format, hint=None): if format: ext = '.' + format outfile = os.path.join(outdir,base_prefix+base+ext) if os.path.exists(outfile) and os.path.samefile(infile, outfile): - if hint: raise Abort("calmagick: --outfile same as input, aborting") + if hint: raise lib.Abort("calmagick: --outfile same as input, aborting") outfile = os.path.join(outdir,base_prefix+base+'_calmagick'+ext) return outfile @@ -523,7 +525,7 @@ def compose_calendar(img, outimg, options, callirhoe_args, magick_args, stats=No if '/' in options.ratio: tmp = options.ratio.split('/') - calratio = float(atoi(tmp[0],1))/atoi(tmp[1],1) + calratio = float(lib.atoi(tmp[0],1))/lib.atoi(tmp[1],1) else: calratio = float(options.ratio) if options.placement == 'min' or options.placement == 'max': @@ -596,9 +598,9 @@ def parse_range(s,hint=None): """ if '/' in s: t = s.split('/') - month,span = parse_month_range(t[0]) + month,span = lib.parse_month_range(t[0]) if hint and span == 0: span = hint - year = parse_year(t[1]) + year = lib.parse_year(t[1]) margs = [] for m in xrange(span): margs += [(month,year)] @@ -606,7 +608,7 @@ def parse_range(s,hint=None): if month > 12: month = 1; year += 1 return margs else: - raise Abort("calmagick: invalid range format '%s'" % options.range) + raise lib.Abort("calmagick: invalid range format '%s'" % options.range) def range_worker(q,ev,i): """worker thread for a (I{Month,Year}) tuple @@ -638,7 +640,7 @@ def main_program(): parser = get_parser() magick_args = parse_magick_args() - sys.argv,argv2 = extract_parser_args(sys.argv,parser,2) + sys.argv,argv2 = lib.extract_parser_args(sys.argv,parser,2) (options,args) = parser.parse_args() check_parsed_options(options) @@ -682,12 +684,12 @@ def main_program(): else: img = args[0] if not os.path.isfile(img): - raise Abort("calmagick: input image '%s' does not exist" % img) + raise lib.Abort("calmagick: input image '%s' does not exist" % img) outimg = get_outfile(img,options.outdir,'',options.format,options.outfile) compose_calendar(img, outimg, options, argv2, magick_args) if __name__ == '__main__': try: main_program() - except Abort as e: + except lib.Abort as e: sys.exit(e.args[0]) diff --git a/lib/__init__.py b/lib/__init__.py index e69de29..1f32408 100644 --- a/lib/__init__.py +++ b/lib/__init__.py @@ -0,0 +1,108 @@ +import time + +_version = "0.4.1" +_copyright = """Copyright (C) 2012-2014 George M. Tzoumas +License GPLv3+: GNU GPL version 3 or later +This is free software: you are free to change and redistribute it. +There is NO WARRANTY, to the extent permitted by law.""" + +class Abort(Exception): + pass + +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 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 + diff --git a/lib/plugin.py b/lib/plugin.py index 911cc98..76768c2 100644 --- a/lib/plugin.py +++ b/lib/plugin.py @@ -26,6 +26,11 @@ import sys import os.path import glob +try: + import resources +except: + resources = None + def available_files(parent, dir, fmatch = None): """find parent/dir/*.py files to be used for plugins @@ -38,7 +43,7 @@ def available_files(parent, dir, fmatch = None): good = False res = [] pattern = parent + "/" + dir + "/*.py" - for x in glob.glob(pattern): + for x in glob.glob(pattern) if not parent.startswith('resource:') else resources.resource_list[dir]: basex = os.path.basename(x) if basex == "__init__.py": good = True elif basex.startswith('_'): @@ -56,7 +61,10 @@ def plugin_list(cat): @rtype: [str,...] """ plugin_paths = get_plugin_paths() - return available_files(plugin_paths[0], cat) + available_files(plugin_paths[1], cat) + result = [] + for path in plugin_paths: + result += available_files(path, cat) + return result # cat = lang (category) # longcat = language @@ -67,6 +75,9 @@ def plugin_list(cat): def get_plugin_paths(): """return the plugin search paths - @rtype: [str,str] + @rtype: [str,str,..] """ - return [ os.path.expanduser("~/.callirhoe"), sys.path[0] if sys.path[0] else "." ] + result = [ os.path.expanduser("~/.callirhoe"), sys.path[0] if sys.path[0] else "." ] + if resources: + result.append("resource:") + return result diff --git a/scripts/PKGBUILD b/scripts/PKGBUILD new file mode 100644 index 0000000..e91388d --- /dev/null +++ b/scripts/PKGBUILD @@ -0,0 +1,34 @@ +# Maintainer: Nick Kavalieris +# Upstream Author : George Tzoumas +# For contributors check the AUTHORS file + +# This file is also included in the source tree for purposes of completeness +# The normal way of aqusition is from AUR: + +pkgname=callirhoe +pkgver=228 +pkgrel=1 +pkgdesc="PDF Calendar creator with high quality vector graphics" +url="https://code.google.com/p/callirhoe/" +arch=('any') +license=('GPLv3') +depends=('python2' 'imagemagick' 'python2-cairo' 'subversion') +source=("$pkgname::svn+https://callirhoe.googlecode.com/svn/branches/phantome") +md5sums=('SKIP') + + +pkgver() { + cd "$pkgname" + svnversion | tr -d [A-z] | sed 's/ *//g' +} + +build() { + cd "${srcdir}/${pkgname}" + make +} + +package() { + cd "${srcdir}/${pkgname}" + install -Dm644 COPYING "$pkgdir/usr/share/licenses/$pkgname/COPYING" + make DESTDIR="$pkgdir/usr" install +} \ No newline at end of file diff --git a/scripts/make_pkg b/scripts/make_pkg new file mode 100755 index 0000000..3e7b5b9 --- /dev/null +++ b/scripts/make_pkg @@ -0,0 +1,42 @@ +#!/bin/bash + +make_python_zip() { + base="$1" + tempdir="$2" + curdir=`pwd` + for i in `find $tempdir -type f -name "*.py" | grep -v "__init__"`; do + python2.7 -m py_compile $i + rm $i + done + cd $tempdir + zip -q -r $curdir/$base.zip * + cd $curdir + rm -rf $tempdir + echo '#!/usr/bin/env python2.7' | cat - $base.zip > $base + chmod 755 $base + rm -f $base.zip +} + +create_callirhoe_package() { + DIR=`mktemp -d -t callirhoe.XXX` + tar c {geom,lang,layouts,lib,style}/*.py | tar x -C "$DIR" + cp callirhoe.py "$DIR/__main__.py" + python2.7 scripts/make_resources_list.py > "$DIR/lib/resources.py" + + make_python_zip callirhoe "$DIR" +} + +create_calmagick_package() { + # Create Calmagick package + DIR=`mktemp -d -t callirhoe.XXX` + tar c lib/{__init__,geom}.py | tar x -C "$DIR" + cp calmagick.py "$DIR/__main__.py" + + make_python_zip calmagick "$DIR" +} + +set -e + +cd .. +[[ -x callirhoe ]] && echo "callirhoe package seems to exist, skipping; rm 'calliroe' to recreate" || create_callirhoe_package +[[ -x calmagick ]] && echo "calmagick package seems to exist, skipping; rm 'calmagick' to recreate" || create_calmagick_package diff --git a/scripts/make_resources_list.py b/scripts/make_resources_list.py new file mode 100644 index 0000000..1b17b1d --- /dev/null +++ b/scripts/make_resources_list.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python2.7 +import glob + +res = dict() + +for x in ['lang', 'style', 'layouts', 'geom']: + res[x] = glob.glob('%s/*.py' % x) + +print 'resource_list = {}' +for x in res.keys(): + print 'resource_list["%s"] = %s' % (x, str(res[x]))