#!/usr/bin/env python # coding=utf-8 # # Copyright (C) 2008 Aurelio A. Heckert # Week number option added by Olav Vitters and Nicolas Dufour (2012) # # 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 2 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, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # """ A calendar generator plugin for Inkscape, but also can be used as a standalone command line application. # More on ISO week number calculation on: http://en.wikipedia.org/wiki/ISO_week_date (The first week of a year is the week that contains the first Thursdayof the year.) """ __version__ = "0.4" import datetime import calendar import re import sys import inkex from inkex import TextElement from inkex.localization import inkex_gettext as _ if sys.version_info[0] > 2: def unicode(stringlike, encoding): """Compatibility for python 2 strings""" if isinstance(stringlike, bytes): return stringlike.decode(encoding) return stringlike class Calendar(inkex.EffectExtension): """Generate Calendar in SVG""" def add_arguments(self, pars): pars.add_argument("--tab", type=str, dest="tab") pars.add_argument( "--month", type=int, default=0, help="Month to be generated. If 0, then the entry year will be generated.", ) pars.add_argument( "--year", type=int, default=0, help="Year to be generated. If 0, then the current year will be generated.", ) pars.add_argument( "--fill-empty-day-boxes", type=inkex.Boolean, dest="fill_edb", default=True, help="Fill empty day boxes with next month days.", ) pars.add_argument( "--show-week-number", type=inkex.Boolean, dest="show_weeknr", default=False, help="Include a week number column.", ) pars.add_argument( "--start-day", default="sun", help='Week start day. ("sun" or "mon")' ) pars.add_argument( "--weekend", default="sat+sun", help='Define the weekend days. ("sat+sun" or "sat" or "sun")', ) pars.add_argument( "--auto-organize", type=inkex.Boolean, dest="auto_organize", default=True, help="Automatically set the size and positions.", ) pars.add_argument( "--months-per-line", type=int, dest="months_per_line", default=3, help="Number of months side by side.", ) pars.add_argument( "--month-width", type=str, dest="month_width", default="6cm", help="The width of the month days box.", ) pars.add_argument( "--month-margin", type=str, dest="month_margin", default="1cm", help="The space between the month boxes.", ) pars.add_argument( "--color-year", type=inkex.Color, dest="color_year", default=inkex.Color(0x808080FF), help="Color for the year header.", ) pars.add_argument( "--color-month", type=inkex.Color, dest="color_month", default=inkex.Color(0x686868FF), help="Color for the month name header.", ) pars.add_argument( "--color-day-name", type=inkex.Color, dest="color_day_name", default=inkex.Color(0x909090FF), help="Color for the week day names header.", ) pars.add_argument( "--color-day", type=inkex.Color, dest="color_day", default=inkex.Color(0x000000FF), help="Color for the common day box.", ) pars.add_argument( "--color-weekend", type=inkex.Color, dest="color_weekend", default=inkex.Color(0x787878FF), help="Color for the weekend days.", ) pars.add_argument( "--color-nmd", type=inkex.Color, dest="color_nmd", default=inkex.Color(0xB0B0B0FF), help="Color for the next month day, in empty day boxes.", ) pars.add_argument( "--color-weeknr", type=inkex.Color, dest="color_weeknr", default=inkex.Color(0x787878FF), help="Color for the week numbers.", ) pars.add_argument( "--font-year", type=str, dest="font_year", default="arial", help="Font for the year string.", ) pars.add_argument( "--font-month", type=str, dest="font_month", default="arial", help="Font for the month strings.", ) pars.add_argument( "--font-day-name", type=str, dest="font_day_name", default="arial", help="Font for the days of the week strings.", ) pars.add_argument( "--font-day", type=str, dest="font_day", default="arial", help="Font for the day strings.", ) pars.add_argument( "--month-names", type=str, dest="month_names", default="January February March " "April May June July " "August September October " "November December", help="The month names for localization.", ) pars.add_argument( "--day-names", type=str, dest="day_names", default="Sun Mon Tue Wed Thu Fri Sat", help="The week day names for localization.", ) pars.add_argument( "--weeknr-name", type=str, dest="weeknr_name", default="Wk", help="The week number column name for localization.", ) pars.add_argument( "--encoding", type=str, dest="input_encode", default="arabic", help="The input encoding of the names.", ) def validate_options(self): """Validity check for various text inputs""" # inkex.errormsg( self.options.input_encode ) # Convert string names lists in real lists month_name = re.match(r"\s*(.*[^\s])\s*", self.options.month_names) self.options.month_names = re.split(r"\s+", month_name.group(1)) month_name = re.match(r"\s*(.*[^\s])\s*", self.options.day_names) self.options.day_names = re.split(r"\s+", month_name.group(1)) # Validate names lists if len(self.options.month_names) != 12: inkex.errormsg( 'The month name list "' + str(self.options.month_names) + '" is invalid. Using default.' ) self.options.month_names = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", ] if len(self.options.day_names) != 7: inkex.errormsg( 'The day name list "' + str(self.options.day_names) + '" is invalid. Using default.' ) self.options.day_names = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] # Convert year 0 to current year if self.options.year == 0: self.options.year = datetime.datetime.today().year # Year 1 starts it's week at monday, obligatorily if self.options.year == 1: self.options.start_day = "mon" # Set the calendar start day if self.options.start_day == "sun": calendar.setfirstweekday(6) else: calendar.setfirstweekday(0) # Convert string numbers with unit to user space float numbers self.options.month_width = self.svg.unittouu(self.options.month_width) self.options.month_margin = self.svg.unittouu(self.options.month_margin) # initial values month_x_pos = 0 month_y_pos = 0 weeknr = 0 def calculate_size_and_positions(self): """month_margin month_width months_per_line auto_organize""" self.doc_w = self.svg.unittouu(self.document.getroot().get("width")) self.doc_h = self.svg.unittouu(self.document.getroot().get("height")) if self.options.show_weeknr: self.cols_before = 1 else: self.cols_before = 0 if self.options.auto_organize: if self.doc_h > self.doc_w: self.months_per_line = 3 else: self.months_per_line = 4 else: self.months_per_line = self.options.months_per_line # self.month_w = self.doc_w / self.months_per_line if self.options.auto_organize: self.month_w = (self.doc_w * 0.8) / self.months_per_line self.month_margin = self.month_w / 10 else: self.month_w = self.options.month_width self.month_margin = self.options.month_margin self.day_w = self.month_w / (7 + self.cols_before) self.day_h = self.month_w / 9 self.month_h = self.day_w * 7 if self.options.month == 0: self.year_margin = ( self.doc_w + self.day_w - (self.month_w * self.months_per_line) - (self.month_margin * (self.months_per_line - 1)) ) / 2 # - self.month_margin else: self.year_margin = (self.doc_w - self.month_w) / 2 self.style_day = { "font-size": str(self.day_w / 2), "font-family": self.options.font_day, "text-anchor": "middle", "text-align": "center", "fill": self.options.color_day, } self.style_weekend = self.style_day.copy() self.style_weekend["fill"] = self.options.color_weekend self.style_nmd = self.style_day.copy() self.style_nmd["fill"] = self.options.color_nmd self.style_month = self.style_day.copy() self.style_month["fill"] = self.options.color_month self.style_month["font-family"] = self.options.font_month self.style_month["font-size"] = str(self.day_w / 1.5) self.style_month["font-weight"] = "bold" self.style_day_name = self.style_day.copy() self.style_day_name["fill"] = self.options.color_day_name self.style_day_name["font-family"] = self.options.font_day_name self.style_day_name["font-size"] = str(self.day_w / 3) self.style_year = self.style_day.copy() self.style_year["fill"] = self.options.color_year self.style_year["font-family"] = self.options.font_year self.style_year["font-size"] = str(self.day_w * 2) self.style_year["font-weight"] = "bold" self.style_weeknr = self.style_day.copy() self.style_weeknr["fill"] = self.options.color_weeknr self.style_weeknr["font-size"] = str(self.day_w / 3) def is_weekend(self, pos): """Detect weekend days; weekend values: "sat+sun" or "sat" or "sun" """ if self.options.start_day == "sun": if self.options.weekend == "sat+sun" and pos == 0: return True if self.options.weekend == "sat+sun" and pos == 6: return True if self.options.weekend == "sat" and pos == 6: return True if self.options.weekend == "sun" and pos == 0: return True else: if self.options.weekend == "sat+sun" and pos == 5: return True if self.options.weekend == "sat+sun" and pos == 6: return True if self.options.weekend == "sat" and pos == 5: return True if self.options.weekend == "sun" and pos == 6: return True return False def in_line_month(self, cal): """Lay out days of week in each week""" cal2 = [] for week in cal: for day in week: if day != 0: cal2.append(day) return cal2 def write_month_header(self, g, m): """Write month header""" txt_atts = { "style": str(inkex.Style(self.style_month)), "x": str((self.month_w - self.day_w) / 2), "y": str(self.day_h / 5), } try: g.add(TextElement(**txt_atts)).text = unicode( self.options.month_names[m - 1], self.options.input_encode ) except: raise ValueError(_("You must select a correct system encoding.")) week_group = g.add(inkex.Group()) week_x = 0 if self.options.start_day == "sun": day_names = self.options.day_names[:] else: day_names = self.options.day_names[1:] day_names.append(self.options.day_names[0]) if self.options.show_weeknr: day_names.insert(0, self.options.weeknr_name) for wday in day_names: txt_atts = { "style": str(inkex.Style(self.style_day_name)), "x": str(self.day_w * week_x), "y": str(self.day_h), } try: week_group.add(TextElement(**txt_atts)).text = unicode( wday, self.options.input_encode ) except: raise ValueError(_("You must select a correct system encoding.")) week_x += 1 def create_month(self, m): """Lay out one month in place on the calendar""" txt_atts = { "transform": "translate(" + str( self.year_margin + (self.month_w + self.month_margin) * self.month_x_pos ) + "," + str((self.day_h * 4) + (self.month_h * self.month_y_pos)) + ")", "id": "month_" + str(m) + "_" + str(self.options.year), } g = self.year_g.add(inkex.Group(**txt_atts)) self.write_month_header(g, m) gdays = g.add(inkex.Group()) cal = calendar.monthcalendar(self.options.year, m) if m == 1: if self.options.year > 1: before_month = self.in_line_month( calendar.monthcalendar(self.options.year - 1, 12) ) else: before_month = self.in_line_month( calendar.monthcalendar(self.options.year, m - 1) ) if m == 12: next_month = self.in_line_month( calendar.monthcalendar(self.options.year + 1, 1) ) else: next_month = self.in_line_month( calendar.monthcalendar(self.options.year, m + 1) ) if len(cal) < 6: # add a line after the last week cal.append([0, 0, 0, 0, 0, 0, 0]) if len(cal) < 6: # add a line before the first week (Feb 2009) cal.reverse() cal.append([0, 0, 0, 0, 0, 0, 0]) cal.reverse() # How mutch before month days will be showed: bmd = cal[0].count(0) + cal[1].count(0) before = True week_y = 0 for week in cal: if ( self.weeknr != 0 and ( (self.options.start_day == "mon" and week[0] != 0) or (self.options.start_day == "sun" and week[1] != 0) ) ) or ( self.weeknr == 0 and ( (self.options.start_day == "mon" and week[3] > 0) or (self.options.start_day == "sun" and week[4] > 0) ) ): self.weeknr += 1 week_x = 0 if self.options.show_weeknr: # Remove leap week (starting previous year) and empty weeks if self.weeknr != 0 and not (week[0] == 0 and week[6] == 0): style = self.style_weeknr txt_atts = { "style": str(inkex.Style(style)), "x": str(self.day_w * week_x), "y": str(self.day_h * (week_y + 2)), } gdays.add(TextElement(**txt_atts)).text = str(self.weeknr) week_x += 1 else: week_x += 1 for day in week: style = self.style_day if self.is_weekend(week_x - self.cols_before): style = self.style_weekend if day == 0: style = self.style_nmd txt_atts = { "style": str(inkex.Style(style)), "x": str(self.day_w * week_x), "y": str(self.day_h * (week_y + 2)), } text = None if day == 0 and not self.options.fill_edb: pass # draw nothing elif day == 0: if before: text = str(before_month[-bmd]) bmd -= 1 else: text = str(next_month[bmd]) bmd += 1 else: text = str(day) before = False if text: gdays.add(TextElement(**txt_atts)).text = text week_x += 1 week_y += 1 self.month_x_pos += 1 if self.month_x_pos >= self.months_per_line: self.month_x_pos = 0 self.month_y_pos += 1 def effect(self): self.validate_options() self.calculate_size_and_positions() parent = self.document.getroot() txt_atts = {"id": "year_" + str(self.options.year)} self.year_g = parent.add(inkex.Group(**txt_atts)) txt_atts = { "style": str(inkex.Style(self.style_year)), "x": str(self.doc_w / 2), "y": str(self.day_w * 1.5), } self.year_g.add(TextElement(**txt_atts)).text = str(self.options.year) try: if self.options.month == 0: for m in range(1, 13): self.create_month(m) else: self.create_month(self.options.month) except ValueError as err: return inkex.errormsg(str(err)) return None if __name__ == "__main__": Calendar().run()