diff --git a/docs/about.rst b/docs/about.rst index 1221d34..f87e028 100644 --- a/docs/about.rst +++ b/docs/about.rst @@ -13,6 +13,5 @@ Inkycal is free to use for anyone (non-commercially) and open-source. It is mainly developed by `aceisace `_ and a few other developers in their free time. -Developing Inkycal requires a fairly large amount of coffee and the ePaper displays aren't free -either. Please consider a `DONATION `_ to help keep this project +Developing Inkycal requires a fairly large amount of coffee and free time. We work in our free time for offer you the best software we can write. Please consider a `DONATION `_ to help keep this project well-maintained |:person_bowing:| . diff --git a/docs/conf.py b/docs/conf.py index 4309f70..d3947e1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,7 +22,7 @@ copyright = '2020, Ace Isace' author = 'Ace Isace' # The full version, including alpha/beta/rc tags -release = '2.0.0beta' +release = '2.0.0' # -- General configuration --------------------------------------------------- @@ -46,7 +46,7 @@ exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'alabaster' +html_theme = 'classic' # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, diff --git a/docs/inkycal.rst b/docs/inkycal.rst index 4c1abb6..a29ce7c 100644 --- a/docs/inkycal.rst +++ b/docs/inkycal.rst @@ -8,18 +8,6 @@ Inkycal :members: .. - inkycal settings - =================== - .. module:: inkycal.config.settings_parser - .. autoclass:: Settings - :members: - - inkycal layout - =================== - .. module:: inkycal.config.layout - .. autoclass:: Layout - :members: - inkycal calendar =================== .. module:: inkycal.modules.inkycal_calendar @@ -32,10 +20,10 @@ Inkycal .. autoclass:: Agenda :members: - inkycal rss + inkycal feeds =================== - .. module:: inkycal.modules.inkycal_rss - .. autoclass:: RSS + .. module:: inkycal.modules.inkycal_feeds + .. autoclass:: Feeds :members: inkycal weather diff --git a/inkycal/__init__.py b/inkycal/__init__.py index e5bfd50..f3b884b 100644 --- a/inkycal/__init__.py +++ b/inkycal/__init__.py @@ -1,13 +1,12 @@ -# Settings and Layout -#from inkycal.config.layout import Layout -#from inkycal.config.settings_parser import Settings +# Display class (for dirving E-Paper displays) from inkycal.display import Display -# All supported inkycal_modules +# Default modules import inkycal.modules.inkycal_agenda import inkycal.modules.inkycal_calendar import inkycal.modules.inkycal_weather import inkycal.modules.inkycal_feeds +import inkycal.modules.inkycal_todoist # import inkycal.modules.inkycal_image # import inkycal.modules.inkycal_server diff --git a/inkycal/config/__init__.py b/inkycal/config/__init__.py deleted file mode 100644 index 9109855..0000000 --- a/inkycal/config/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .layout import Layout diff --git a/inkycal/config/layout.py b/inkycal/config/layout.py deleted file mode 100644 index 29976f0..0000000 --- a/inkycal/config/layout.py +++ /dev/null @@ -1,127 +0,0 @@ -#!/usr/bin/python3 -# -*- coding: utf-8 -*- -""" -Layout module for Inky-Calendar software. -Copyright by aceisace -""" - -import logging -import os - -filename = os.path.basename(__file__).split('.py')[0] -logger = logging.getLogger(filename) -logger.setLevel(level=logging.INFO) - -class Layout: - """Page layout handling""" - - def __init__(self, model=None, width=None, height=None, - supports_colour=False, use_info_section=True): - """Initialize parameters for specified epaper model - Use model parameter to specify display OR - Crate a custom display with given width and height""" - - if (model != None) and (width == None) and (height == None): - display_dimensions = { - '9_in_7': (1200, 825), - 'epd_7_in_5_v2_colour': (800, 480), - 'epd_7_in_5_v2': (800, 480), - 'epd_7_in_5_colour': (640, 384), - 'epd_7_in_5': (640, 384), - 'epd_5_in_83_colour': (600, 448), - 'epd_5_in_83': (600, 448), - 'epd_4_in_2_colour': (400, 300), - 'epd_4_in_2': (400, 300) - } - - self.display_height, self.display_width = display_dimensions[model] - - # if 'colour' was found in the display name, set supports_colour to True - if 'colour' in model: - self.supports_colour = True - else: - self.supports_colour = False - - # If a custom width and height was specified, use those values instead - elif width and height: - self.display_height = width - self.display_width = height - self.supports_colour = supports_colour - - else: - raise Exception("Can't create a layout without given sizes") - - # If the info section should be used, reduce the canvas size to 95% - if not isinstance(use_info_section, bool): - raise ValueError('use_info_section should be a boolean (True/False)') - - if use_info_section == True: - self.display_height = int(self.display_height*0.95) - - self.display_size = self.display_width, self.display_height - - self.top_section_width = self.display_width - self.middle_section_width = self.display_width - self.bottom_section_width = self.display_width - self.create_sections() - - def create_sections(self, top_section=0.10, middle_section=0.65, - bottom_section=0.25): - """Allocate fixed percentage height for top and middle section - e.g. 0.2 = 20% (Leave empty for default values) - Set top/bottom_section to 0 to allocate more space for the middle section - """ - scale = lambda percentage: round(percentage * self.display_height) - - if top_section == 0 or bottom_section == 0: - if top_section == 0: - self.top_section_height = 0 - - if bottom_section == 0: - self.bottom_section_height = 0 - - self.middle_section_height = scale(1 - top_section - bottom_section) - else: - if top_section + middle_section + bottom_section > 1.0: - print('All percentages should add up to max 100%, not more!') - raise - - self.top_section_height = scale(top_section) - self.middle_section_height = scale(middle_section) - self.bottom_section_height = (self.display_height - - self.top_section_height - self.middle_section_height) - - logger.debug(('top-section size: {} x {} px'.format( - self.top_section_width, self.top_section_height))) - logger.debug(('middle-section size: {} x {} px'.format( - self.middle_section_width, self.middle_section_height))) - logger.debug(('bottom-section size: {} x {} px'.format( - self.bottom_section_width, self.bottom_section_height))) - - - def get_size(self, section): - """Enter top/middle/bottom to get the size of the section as a tuple: - (width, height)""" - - if section not in ['top','middle','bottom']: - raise Exception('Invalid section: ', section) - else: - if section == 'top': - size = (self.top_section_width, self.top_section_height) - elif section == 'middle': - size = (self.middle_section_width, self.middle_section_height) - elif section == 'bottom': - size = (self.bottom_section_width, self.bottom_section_height) - return size - -## def set_info_section(self, value): -## """Should a small info section be showed """ -## if not isinstance(value, bool): -## raise ValueError('value has to bee a boolean: True/False') -## self.info_section = value -## logger.info(('show info section: {}').format(value)) - - -if __name__ == '__main__': - print('running {0} in standalone/debug mode'.format( - os.path.basename(__file__).split('.py')[0])) \ No newline at end of file diff --git a/inkycal/display/display.py b/inkycal/display/display.py index cb11474..92e7e1c 100644 --- a/inkycal/display/display.py +++ b/inkycal/display/display.py @@ -27,8 +27,6 @@ class Display: driver = import_module(driver_path) self._epaper = driver.EPD() self.model_name = epaper_model - #self.height = driver.EPD_HEIGHT - #self.width = driver.EPD_WIDTH except ImportError: raise Exception('This module is not supported. Check your spellings?') diff --git a/inkycal/display/drivers/9_in_7_drivers/setup_state.txt b/inkycal/display/drivers/9_in_7_drivers/setup_state.txt index 56a6051..573541a 100644 --- a/inkycal/display/drivers/9_in_7_drivers/setup_state.txt +++ b/inkycal/display/drivers/9_in_7_drivers/setup_state.txt @@ -1 +1 @@ -1 \ No newline at end of file +0 diff --git a/inkycal/main.py b/inkycal/main.py index 9a18209..1270c8f 100644 --- a/inkycal/main.py +++ b/inkycal/main.py @@ -52,7 +52,6 @@ class Inkycal: with open(settings_path) as file: settings = json.load(file) self.settings = settings - #print(self.settings) except FileNotFoundError: print('No settings file found in specified location') diff --git a/inkycal/modules/__init__.py b/inkycal/modules/__init__.py index c642da6..822a2bd 100644 --- a/inkycal/modules/__init__.py +++ b/inkycal/modules/__init__.py @@ -1,5 +1,7 @@ from .inkycal_agenda import Agenda from .inkycal_calendar import Calendar from .inkycal_weather import Weather -from .inkycal_feeds import RSS +from .inkycal_feeds import Feeds +from .inkycal_todoist import Todoist #from .inkycal_image import Image +#from .inkycal_server import Server diff --git a/inkycal/modules/test_module.py b/inkycal/modules/dev_module.py similarity index 97% rename from inkycal/modules/test_module.py rename to inkycal/modules/dev_module.py index c79b41f..d8877be 100644 --- a/inkycal/modules/test_module.py +++ b/inkycal/modules/dev_module.py @@ -1,7 +1,9 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- """ -Developer module template for Inkycal Project +Module template for Inky-Calendar Project + +Create your own module with this template Copyright by aceisace """ @@ -77,6 +79,9 @@ class Simple(inkycal_module): # Initialise this module via the inkycal_module template (required) super().__init__(section_size, section_config) + # module name (required) + self.name = self.__class__.__name__ + # module specific parameters (optional) self.do_something = True diff --git a/inkycal/modules/inkycal_agenda.py b/inkycal/modules/inkycal_agenda.py index 106a2ee..b1f191c 100644 --- a/inkycal/modules/inkycal_agenda.py +++ b/inkycal/modules/inkycal_agenda.py @@ -14,7 +14,7 @@ import arrow filename = os.path.basename(__file__).split('.py')[0] logger = logging.getLogger(filename) -logger.setLevel(level=logging.INFO) +logger.setLevel(level=logging.ERROR) class Agenda(inkycal_module): """Agenda class @@ -243,4 +243,4 @@ class Agenda(inkycal_module): return im_black, im_colour if __name__ == '__main__': - print('running {0} in standalone mode'.format(filename)) \ No newline at end of file + print('running {0} in standalone mode'.format(filename)) diff --git a/inkycal/modules/inkycal_calendar.py b/inkycal/modules/inkycal_calendar.py index 79637fa..748f2ee 100644 --- a/inkycal/modules/inkycal_calendar.py +++ b/inkycal/modules/inkycal_calendar.py @@ -12,7 +12,7 @@ import arrow filename = os.path.basename(__file__).split('.py')[0] logger = logging.getLogger(filename) -logger.setLevel(level=logging.DEBUG) +logger.setLevel(level=logging.ERROR) class Calendar(inkycal_module): """Calendar class @@ -22,7 +22,7 @@ class Calendar(inkycal_module): name = "Inkycal Calendar" optional = { - + "week_starts_on" : { "label":"When does your week start? (default=Monday)", "options": ["Monday", "Sunday"], @@ -42,7 +42,7 @@ class Calendar(inkycal_module): "ical_files" : { "label":"iCalendar filepaths, separated with a comma", }, - + "date_format":{ "label":"Use an arrow-supported token for custom date formatting "+ "see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. D MMM", @@ -54,7 +54,7 @@ class Calendar(inkycal_module): "see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. HH:mm", "default": "HH:mm" }, - + } def __init__(self, config): @@ -74,12 +74,12 @@ class Calendar(inkycal_module): self.ical_urls = config['ical_urls'].split(',') else: self.ical_urls = [] - + if config['ical_files'] != "": self.ical_files = config['ical_files'].split(',') else: self.ical_files = [] - + # additional configuration self.timezone = get_system_tz() self.num_font = ImageFont.truetype( @@ -320,4 +320,4 @@ class Calendar(inkycal_module): return im_black, im_colour if __name__ == '__main__': - print('running {0} in standalone mode'.format(filename)) \ No newline at end of file + print('running {0} in standalone mode'.format(filename)) diff --git a/inkycal/modules/inkycal_feeds.py b/inkycal/modules/inkycal_feeds.py index 24143f1..135f601 100644 --- a/inkycal/modules/inkycal_feeds.py +++ b/inkycal/modules/inkycal_feeds.py @@ -18,9 +18,9 @@ except ImportError: filename = os.path.basename(__file__).split('.py')[0] logger = logging.getLogger(filename) -logger.setLevel(level=logging.INFO) +logger.setLevel(level=logging.ERROR) -class RSS(inkycal_module): +class Feeds(inkycal_module): """RSS class parses rss/atom feeds from given urls """ @@ -35,7 +35,7 @@ class RSS(inkycal_module): } optional = { - + "shuffle_feeds": { "label": "Should the parsed RSS feeds be shuffled? (default=True)", "options": [True, False], @@ -61,7 +61,7 @@ class RSS(inkycal_module): # optional parameters self.shuffle_feeds = bool(self.config["shuffle_feeds"]) - + # give an OK message print('{0} loaded'.format(filename)) @@ -149,4 +149,4 @@ class RSS(inkycal_module): return im_black, im_colour if __name__ == '__main__': - print('running {0} in standalone/debug mode'.format(filename)) \ No newline at end of file + print('running {0} in standalone/debug mode'.format(filename)) diff --git a/inkycal/modules/inkycal_image.py b/inkycal/modules/inkycal_image.py index 77ec423..fddb915 100644 --- a/inkycal/modules/inkycal_image.py +++ b/inkycal/modules/inkycal_image.py @@ -1,32 +1,309 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- + """ -Image module for inkycal Project +Image module for Inkycal Project Copyright by aceisace -Development satge: Beta """ -from os import path +from inkycal.modules.template import inkycal_module +from inkycal.custom import * + from PIL import ImageOps import requests import numpy -"""----------------------------------------------------------------""" -#path = 'https://github.com/aceisace/Inky-Calendar/raw/master/Gallery/Inky-Calendar-logo.png' -#path ='/home/pi/Inky-Calendar/images/canvas.png' -path = inkycal_image_path -path_body = inkycal_image_path_body -mode = 'auto' # 'horizontal' # 'vertical' # 'auto' -upside_down = False # Flip image by 180 deg (upside-down) -alignment = 'center' # top_center, top_left, center_left, bottom_right etc. -colours = 'bwr' # bwr # bwy # bw -render = True # show image on E-Paper? -"""----------------------------------------------------------------""" +filename = os.path.basename(__file__).split('.py')[0] +logger = logging.getLogger(filename) +logger.setLevel(level=logging.ERROR) -# First determine dimensions -if mode == 'horizontal': - display_width, display_height == display_height, display_width +class Inkyimage(inkycal_module): + """Image class + display an image from a given path or URL + """ + + name = "Inykcal Image - show an image from a URL or local path" + + requires = { + 'path': { + "label":"Please enter the path of the image file (local or URL)", + } + + } + + optional = { + + 'rotation':{ + "label":"Specify the angle to rotate the image. Default is 0", + "options": [0, 90, 180, 270, 360, "auto"], + "default":0, + }, + + 'layout':{ + "label":"How should the image be displayed on the display? Default is auto", + "options": ['fill', 'center', 'fit', 'auto'], + "default": "auto" + } + + } + + + def __init__(self, config): + """Initialize inkycal_rss module""" + + super().__init__(config) + + config = config['config'] + + # required parameters + for param in self.requires: + if not param in config: + raise Exception('config is missing {}'.format(param)) + + # optional parameters + self.image_path = self.config['path'] + + self.rotation = self.config['rotation'] + self.layout = self.config['layout'] + + # give an OK message + print('{0} loaded'.format(self.name)) + + def _validate(self): + """Validate module-specific parameters""" + + # Validate image_path + if not isinstance(self.image_path, str): + print( + 'image_path has to be a string: "URL1" or "/home/pi/Desktop/im.png"') + + # Validate layout + if not isinstance(self.layout, str): + print('layout has to be a string') + + def generate_image(self): + """Generate image for this module""" + + # Define new image size with respect to padding + im_width = self.width + im_height = self.height + im_size = im_width, im_height + logger.info('image size: {} x {} px'.format(im_width, im_height)) + + # Try to open the image if it exists and is an image file + try: + if self.image_path.startswith('http'): + logger.debug('identified url') + self.image = Image.open(requests.get(self.image_path, stream=True).raw) + else: + logger.info('identified local path') + self.image = Image.open(self.image_path) + except FileNotFoundError: + raise ('Your file could not be found. Please check the filepath') + except OSError: + raise ('Please check if the path points to an image file.') + + logger.debug(('image-width:', self.image.width)) + logger.debug(('image-height:', self.image.height)) + + # Create an image for black pixels and one for coloured pixels + im_black = Image.new('RGB', size = im_size, color = 'white') + im_colour = Image.new('RGB', size = im_size, color = 'white') + + # do the required operations + self._remove_alpha() + self._to_layout() + black, colour = self._map_colours() + + # paste the images on the canvas + im_black.paste(black, (self.x, self.y)) + im_colour.paste(colour, (self.x, self.y)) + + # Save images of black and colour channel in image-folder + im_black.save(images+self.name+'.png', 'PNG') + im_colour.save(images+self.name+'_colour.png', 'PNG') + + def _rotate(self, angle=None): + """Rotate the image to a given angle + angle must be one of :[0, 90, 180, 270, 360, 'auto'] + """ + im = self.image + if angle == None: + angle = self.rotation + + # Check if angle is supported + if angle not in self._allowed_rotation: + print('invalid angle provided, setting to fallback: 0 deg') + angle = 0 + + # Autoflip the image if angle == 'auto' + if angle == 'auto': + if (im.width > self.height) and (im.width < self.height): + print('display vertical, image horizontal -> flipping image') + image = im.rotate(90, expand=True) + if (im.width < self.height) and (im.width > self.height): + print('display horizontal, image vertical -> flipping image') + image = im.rotate(90, expand=True) + # if not auto, flip to specified angle + else: + image = im.rotate(angle, expand = True) + self.image = image + + def _fit_width(self, width=None): + """Resize an image to desired width""" + im = self.image + if width == None: width = self.width + + logger.debug(('resizing width from', im.width, 'to')) + wpercent = (width/float(im.width)) + hsize = int((float(im.height)*float(wpercent))) + image = im.resize((width, hsize), Image.ANTIALIAS) + logger.debug(image.width) + self.image = image + + def _fit_height(self, height=None): + """Resize an image to desired height""" + im = self.image + if height == None: height = self.height + + logger.debug(('resizing height from', im.height, 'to')) + hpercent = (height / float(im.height)) + wsize = int(float(im.width) * float(hpercent)) + image = im.resize((wsize, height), Image.ANTIALIAS) + logger.debug(image.height) + self.image = image + + def _to_layout(self, mode=None): + """Adjust the image to suit the layout + mode can be center, fit or fill""" + + im = self.image + if mode == None: mode = self.layout + + if mode not in self._allowed_layout: + print('{} is not supported. Should be one of {}'.format( + mode, self._allowed_layout)) + print('setting layout to fallback: centre') + mode = 'center' + + # If mode is center, just center the image + if mode == 'center': + pass + + # if mode is fit, adjust height of the image while keeping ascept-ratio + if mode == 'fit': + self._fit_height() + + # if mode is fill, enlargen or shrink the image to fit width + if mode == 'fill': + self._fit_width() + + # in auto mode, flip image automatically and fit both height and width + if mode == 'auto': + + # Check if width is bigger than height and rotate by 90 deg if true + if im.width > im.height: + self._rotate(90) + + # fit both height and width + self._fit_height() + self._fit_width() + + if self.image.width > self.width: + x = int( (self.image.width - self.width) / 2) + else: + x = int( (self.width - self.image.width) / 2) + + if self.image.height > self.height: + y = int( (self.image.height - self.height) / 2) + else: + y = int( (self.height - self.image.height) / 2) + + self.x, self.y = x, y + + def _remove_alpha(self): + im = self.image + + if len(im.getbands()) == 4: + logger.debug('removing transparency') + bg = Image.new('RGBA', (im.width, im.height), 'white') + im = Image.alpha_composite(bg, im) + self.image.paste(im, (0,0)) + + def _map_colours(self, colours = None): + """Map image colours to display-supported colours """ + im = self.image.convert('RGB') + + if colours == 'bw': + + # For black-white images, use monochrome dithering + im_black = im.convert('1', dither=True) + im_colour = None + + elif colours == 'bwr': + # For black-white-red images, create corresponding palette + pal = [255,255,255, 0,0,0, 255,0,0, 255,255,255] + + elif colours == 'bwy': + # For black-white-yellow images, create corresponding palette""" + pal = [255,255,255, 0,0,0, 255,255,0, 255,255,255] + + # Map each pixel of the opened image to the Palette + if colours == 'bwr' or colours == 'bwy': + palette_im = Image.new('P', (3,1)) + palette_im.putpalette(pal * 64) + quantized_im = im.quantize(palette=palette_im) + quantized_im.convert('RGB') + + # Create buffer for coloured pixels + buffer1 = numpy.array(quantized_im.convert('RGB')) + r1,g1,b1 = buffer1[:, :, 0], buffer1[:, :, 1], buffer1[:, :, 2] + + # Create buffer for black pixels + buffer2 = numpy.array(quantized_im.convert('RGB')) + r2,g2,b2 = buffer2[:, :, 0], buffer2[:, :, 1], buffer2[:, :, 2] + + if colours == 'bwr': + # Create image for only red pixels + buffer2[numpy.logical_and(r2 == 0, b2 == 0)] = [255,255,255] # black->white + buffer2[numpy.logical_and(r2 == 255, b2 == 0)] = [0,0,0] #red->black + im_colour = Image.fromarray(buffer2) + + # Create image for only black pixels + buffer1[numpy.logical_and(r1 == 255, b1 == 0)] = [255,255,255] + im_black = Image.fromarray(buffer1) + + if colours == 'bwy': + # Create image for only yellow pixels + buffer2[numpy.logical_and(r2 == 0, b2 == 0)] = [255,255,255] # black->white + buffer2[numpy.logical_and(g2 == 255, b2 == 0)] = [0,0,0] #yellow -> black + im_colour = Image.fromarray(buffer2) + + # Create image for only black pixels + buffer1[numpy.logical_and(g1 == 255, b1 == 0)] = [255,255,255] + im_black = Image.fromarray(buffer1) + + return im_black, im_colour + + @staticmethod + def save(image, path): + im = self.image + im.save(path, 'PNG') + + @staticmethod + def _show(image): + """Preview the image on gpicview (only works on Rapsbian with Desktop)""" + path = '/home/pi/Desktop/' + image.save(path+'temp.png') + os.system("gpicview "+path+'temp.png') + os.system('rm '+path+'temp.png') + +if __name__ == '__main__': + print('running {0} in standalone/debug mode'.format(filename)) + + #a = Inkyimage((480,800), {'path': "https://raw.githubusercontent.com/aceisace/Inky-Calendar/dev_ver2_0/Gallery/logo.png"}) + #a = Inkyimage((480,800), {'path': "https://raw.githubusercontent.com/aceisace/Inky-Calendar/dev_ver2_0/Gallery/logo.png"}) + a = Inkyimage((480, 800), {'path': "/home/pi/Desktop/im/IMG_0475.JPG"}) + a.generate_image() - -print('Done') diff --git a/inkycal/modules/inkycal_image2.py b/inkycal/modules/inkycal_image2.py deleted file mode 100644 index 27f8e17..0000000 --- a/inkycal/modules/inkycal_image2.py +++ /dev/null @@ -1,305 +0,0 @@ -#!/usr/bin/python3 -# -*- coding: utf-8 -*- - -""" -Image module for Inkycal Project -Copyright by aceisace -""" - -from inkycal.modules.template import inkycal_module -from inkycal.custom import * - -from PIL import ImageOps -import requests -import numpy - -filename = os.path.basename(__file__).split('.py')[0] -logger = logging.getLogger(filename) -logger.setLevel(level=logging.ERROR) - -class Inkyimage(inkycal_module): - """Image class - display an image from a given path or URL - """ - - name = "Inykcal Image - show an image from a URL or local path" - - requires = { - 'path': { - "label":"Please enter the path of the image file (local or URL)", - } - - } - - optional = { - 'rotation':{ - "label":"Specify the angle to rotate the image. Default is 0", - "options": [0, 90, 180, 270, 360, "auto"], - "default":0, - }, - 'layout':{ - "label":"How should the image be displayed on the display? Default is auto", - "options": ['fill', 'center', 'fit', 'auto'], - "default": "auto" - } - - } - - - def __init__(self, section_size, section_config): - """Initialize inkycal_rss module""" - - super().__init__(section_size, section_config) - - # required parameters - for param in self.requires: - if not param in section_config: - raise Exception('config is missing {}'.format(param)) - - # optional parameters - self.image_path = self.config['path'] - - self.rotation = self.config['rotation'] - self.layout = self.config['layout'] - - # give an OK message - print('{0} loaded'.format(self.name)) - - def _validate(self): - """Validate module-specific parameters""" - - # Validate image_path - if not isinstance(self.image_path, str): - print( - 'image_path has to be a string: "URL1" or "/home/pi/Desktop/im.png"') - - # Validate layout - if not isinstance(self.layout, str): - print('layout has to be a string') - - def generate_image(self): - """Generate image for this module""" - - # Define new image size with respect to padding - im_width = self.width - im_height = self.height - im_size = im_width, im_height - logger.info('image size: {} x {} px'.format(im_width, im_height)) - - # Try to open the image if it exists and is an image file - try: - if self.image_path.startswith('http'): - logger.debug('identified url') - self.image = Image.open(requests.get(self.image_path, stream=True).raw) - else: - logger.info('identified local path') - self.image = Image.open(self.image_path) - except FileNotFoundError: - raise ('Your file could not be found. Please check the filepath') - except OSError: - raise ('Please check if the path points to an image file.') - - logger.debug(('image-width:', self.image.width)) - logger.debug(('image-height:', self.image.height)) - - # Create an image for black pixels and one for coloured pixels - im_black = Image.new('RGB', size = im_size, color = 'white') - im_colour = Image.new('RGB', size = im_size, color = 'white') - - # do the required operations - self._remove_alpha() - self._to_layout() - black, colour = self._map_colours() - - # paste the images on the canvas - im_black.paste(black, (self.x, self.y)) - im_colour.paste(colour, (self.x, self.y)) - - # Save images of black and colour channel in image-folder - im_black.save(images+self.name+'.png', 'PNG') - im_colour.save(images+self.name+'_colour.png', 'PNG') - - def _rotate(self, angle=None): - """Rotate the image to a given angle - angle must be one of :[0, 90, 180, 270, 360, 'auto'] - """ - im = self.image - if angle == None: - angle = self.rotation - - # Check if angle is supported - if angle not in self._allowed_rotation: - print('invalid angle provided, setting to fallback: 0 deg') - angle = 0 - - # Autoflip the image if angle == 'auto' - if angle == 'auto': - if (im.width > self.height) and (im.width < self.height): - print('display vertical, image horizontal -> flipping image') - image = im.rotate(90, expand=True) - if (im.width < self.height) and (im.width > self.height): - print('display horizontal, image vertical -> flipping image') - image = im.rotate(90, expand=True) - # if not auto, flip to specified angle - else: - image = im.rotate(angle, expand = True) - self.image = image - - def _fit_width(self, width=None): - """Resize an image to desired width""" - im = self.image - if width == None: width = self.width - - logger.debug(('resizing width from', im.width, 'to')) - wpercent = (width/float(im.width)) - hsize = int((float(im.height)*float(wpercent))) - image = im.resize((width, hsize), Image.ANTIALIAS) - logger.debug(image.width) - self.image = image - - def _fit_height(self, height=None): - """Resize an image to desired height""" - im = self.image - if height == None: height = self.height - - logger.debug(('resizing height from', im.height, 'to')) - hpercent = (height / float(im.height)) - wsize = int(float(im.width) * float(hpercent)) - image = im.resize((wsize, height), Image.ANTIALIAS) - logger.debug(image.height) - self.image = image - - def _to_layout(self, mode=None): - """Adjust the image to suit the layout - mode can be center, fit or fill""" - - im = self.image - if mode == None: mode = self.layout - - if mode not in self._allowed_layout: - print('{} is not supported. Should be one of {}'.format( - mode, self._allowed_layout)) - print('setting layout to fallback: centre') - mode = 'center' - - # If mode is center, just center the image - if mode == 'center': - pass - - # if mode is fit, adjust height of the image while keeping ascept-ratio - if mode == 'fit': - self._fit_height() - - # if mode is fill, enlargen or shrink the image to fit width - if mode == 'fill': - self._fit_width() - - # in auto mode, flip image automatically and fit both height and width - if mode == 'auto': - - # Check if width is bigger than height and rotate by 90 deg if true - if im.width > im.height: - self._rotate(90) - - # fit both height and width - self._fit_height() - self._fit_width() - - if self.image.width > self.width: - x = int( (self.image.width - self.width) / 2) - else: - x = int( (self.width - self.image.width) / 2) - - if self.image.height > self.height: - y = int( (self.image.height - self.height) / 2) - else: - y = int( (self.height - self.image.height) / 2) - - self.x, self.y = x, y - - def _remove_alpha(self): - im = self.image - - if len(im.getbands()) == 4: - logger.debug('removing transparency') - bg = Image.new('RGBA', (im.width, im.height), 'white') - im = Image.alpha_composite(bg, im) - self.image.paste(im, (0,0)) - - def _map_colours(self, colours = None): - """Map image colours to display-supported colours """ - im = self.image.convert('RGB') - - if colours == 'bw': - - # For black-white images, use monochrome dithering - im_black = im.convert('1', dither=True) - im_colour = None - - elif colours == 'bwr': - # For black-white-red images, create corresponding palette - pal = [255,255,255, 0,0,0, 255,0,0, 255,255,255] - - elif colours == 'bwy': - # For black-white-yellow images, create corresponding palette""" - pal = [255,255,255, 0,0,0, 255,255,0, 255,255,255] - - # Map each pixel of the opened image to the Palette - if colours == 'bwr' or colours == 'bwy': - palette_im = Image.new('P', (3,1)) - palette_im.putpalette(pal * 64) - quantized_im = im.quantize(palette=palette_im) - quantized_im.convert('RGB') - - # Create buffer for coloured pixels - buffer1 = numpy.array(quantized_im.convert('RGB')) - r1,g1,b1 = buffer1[:, :, 0], buffer1[:, :, 1], buffer1[:, :, 2] - - # Create buffer for black pixels - buffer2 = numpy.array(quantized_im.convert('RGB')) - r2,g2,b2 = buffer2[:, :, 0], buffer2[:, :, 1], buffer2[:, :, 2] - - if colours == 'bwr': - # Create image for only red pixels - buffer2[numpy.logical_and(r2 == 0, b2 == 0)] = [255,255,255] # black->white - buffer2[numpy.logical_and(r2 == 255, b2 == 0)] = [0,0,0] #red->black - im_colour = Image.fromarray(buffer2) - - # Create image for only black pixels - buffer1[numpy.logical_and(r1 == 255, b1 == 0)] = [255,255,255] - im_black = Image.fromarray(buffer1) - - if colours == 'bwy': - # Create image for only yellow pixels - buffer2[numpy.logical_and(r2 == 0, b2 == 0)] = [255,255,255] # black->white - buffer2[numpy.logical_and(g2 == 255, b2 == 0)] = [0,0,0] #yellow -> black - im_colour = Image.fromarray(buffer2) - - # Create image for only black pixels - buffer1[numpy.logical_and(g1 == 255, b1 == 0)] = [255,255,255] - im_black = Image.fromarray(buffer1) - - return im_black, im_colour - - @staticmethod - def save(image, path): - im = self.image - im.save(path, 'PNG') - - @staticmethod - def _show(image): - """Preview the image on gpicview (only works on Rapsbian with Desktop)""" - path = '/home/pi/Desktop/' - image.save(path+'temp.png') - os.system("gpicview "+path+'temp.png') - os.system('rm '+path+'temp.png') - -if __name__ == '__main__': - print('running {0} in standalone/debug mode'.format(filename)) - - #a = Inkyimage((480,800), {'path': "https://raw.githubusercontent.com/aceisace/Inky-Calendar/dev_ver2_0/Gallery/logo.png"}) - #a = Inkyimage((480,800), {'path': "https://raw.githubusercontent.com/aceisace/Inky-Calendar/dev_ver2_0/Gallery/logo.png"}) - a = Inkyimage((480, 800), {'path': "/home/pi/Desktop/im/IMG_0475.JPG"}) - a.generate_image() - - diff --git a/inkycal/modules/inkycal_weather.py b/inkycal/modules/inkycal_weather.py index 596a5e4..f18ec55 100644 --- a/inkycal/modules/inkycal_weather.py +++ b/inkycal/modules/inkycal_weather.py @@ -35,14 +35,14 @@ class Weather(inkycal_module): "api_key" : { "label":"Please enter openweathermap api-key. You can create one for free on openweathermap", }, - + "location": { "label":"Please enter your location in the following format: City, Country-Code" } } optional = { - + "round_temperature": { "label":"Round temperature to the nearest degree?", "options": [True, False], @@ -78,7 +78,7 @@ class Weather(inkycal_module): "options": [True, False], "default": True }, - + } def __init__(self, config): @@ -138,7 +138,7 @@ class Weather(inkycal_module): if not isinstance(self.hour_format, int): print(f'hour_format should be a int, not {self.hour_format}') - + if not isinstance(self.use_beaufort, bool): print(f'use_beaufort should be a int, not {self.use_beaufort}') @@ -373,7 +373,7 @@ class Weather(inkycal_module): ### logger.debug("daily") - + def calculate_forecast(days_from_today): """Get temperature range and most frequent icon code for forecast days_from_today should be int from 1-4: e.g. 2 -> 2 days from today @@ -515,4 +515,4 @@ class Weather(inkycal_module): return im_black, im_colour if __name__ == '__main__': - print('running {0} in standalone mode'.format(filename)) \ No newline at end of file + print('running {0} in standalone mode'.format(filename)) diff --git a/inkycal/tests/inkycal_agenda_test.py b/inkycal/tests/inkycal_agenda_test.py index d7353f3..eb2baac 100644 --- a/inkycal/tests/inkycal_agenda_test.py +++ b/inkycal/tests/inkycal_agenda_test.py @@ -1,27 +1,32 @@ import unittest -from inkycal.modules import Agenda +from inkycal.modules import Agenda as Module -agenda = Agenda( - #size - (400,400), - - # common config - { - 'language': 'en', - 'units': 'metric', - 'hours': 24, - # module-specific config - 'week_starts_on': 'Monday', - 'ical_urls': ['https://calendar.google.com/calendar/ical/en.usa%23holiday%40group.v.calendar.google.com/public/basic.ics'] +test = { + "position": 1, + "name": "Agenda", + "config": { + "size": [880,100], + "ical_urls": "https://www.officeholidays.com/ics-fed/usa", "ical_files": "", + "date_format": "ddd D MMM", + "time_format": "HH:mm", + "padding_x": 10, + "padding_y": 10, + "fontsize": 12, + "language": "en" } - ) +} -class inkycal_agenda_test(unittest.TestCase): +module = Module(test) + +class module_test(unittest.TestCase): + def test_get_config(self): + print('getting data for web-ui') + module.get_config() + def test_generate_image(self): print('testing image generation') - agenda.generate_image() - + module.generate_image() if __name__ == '__main__': unittest.main() diff --git a/inkycal/tests/inkycal_calendar_test.py b/inkycal/tests/inkycal_calendar_test.py index a7fb273..84e2116 100644 --- a/inkycal/tests/inkycal_calendar_test.py +++ b/inkycal/tests/inkycal_calendar_test.py @@ -1,26 +1,34 @@ import unittest -from inkycal.modules import Calendar +from inkycal.modules import Calendar as Module -calendar = Calendar( - #size - (400,400), +test = { + "position": 2, + "name": "Calendar", + "config": { + "size": [880,343], + "week_starts_on": "Monday", + "show_events": "True", + "ical_urls": "https://www.officeholidays.com/ics-fed/usa", + "ical_files": "", + "date_format": "D MMM", + "time_format": "HH:mm", + "padding_x": 10, + "padding_y": 10, + "fontsize": 12, + "language": "en" + } +} - # common config - { - 'language': 'en', - 'units': 'metric', - 'hours': 24, - # module-specific config - 'week_starts_on': 'Monday', - 'ical_urls': ['https://calendar.google.com/calendar/ical/en.usa%23holiday%40group.v.calendar.google.com/public/basic.ics'] - } - ) +module = Module(test) + +class module_test(unittest.TestCase): + def test_get_config(self): + print('getting data for web-ui') + module.get_config() -class inkycal_calendar_test(unittest.TestCase): def test_generate_image(self): print('testing image generation') - calendar.generate_image() - + module.generate_image() if __name__ == '__main__': unittest.main() diff --git a/inkycal/tests/inkycal_feeds_test.py b/inkycal/tests/inkycal_feeds_test.py new file mode 100644 index 0000000..b0e9ea2 --- /dev/null +++ b/inkycal/tests/inkycal_feeds_test.py @@ -0,0 +1,30 @@ +import unittest +from inkycal.modules import Feeds as Module + +test = { + "position": 1, + "name": "Feeds", + "config": { + "size": [400,100], + "feed_urls": "http://feeds.bbci.co.uk/news/world/rss.xml#", + "shuffle_feeds": "True", + "padding_x": 10, + "padding_y": 10, + "fontsize": 12, + "language": "en" + } +} + +module = Module(test) + +class module_test(unittest.TestCase): + def test_get_config(self): + print('getting data for web-ui') + module.get_config() + + def test_generate_image(self): + print('testing image generation') + module.generate_image() + +if __name__ == '__main__': + unittest.main() diff --git a/inkycal/tests/inkycal_rss_test.py b/inkycal/tests/inkycal_rss_test.py deleted file mode 100644 index 5905fe5..0000000 --- a/inkycal/tests/inkycal_rss_test.py +++ /dev/null @@ -1,26 +0,0 @@ -import unittest -from inkycal.modules import RSS - -rss = RSS( - #size - (400,400), - - # common onfig - { - 'language': 'en', - 'units': 'metric', - 'hours': 24, - # module-specific config - 'rss_urls': ['http://feeds.bbci.co.uk/news/world/rss.xml#'] - } - - ) - -class inkycal_rss_test(unittest.TestCase): - def test_generate_image(self): - print('testing image generation') - rss.generate_image() - - -if __name__ == '__main__': - unittest.main() diff --git a/inkycal/tests/inkycal_todo.py b/inkycal/tests/inkycal_todo.py new file mode 100644 index 0000000..22045ae --- /dev/null +++ b/inkycal/tests/inkycal_todo.py @@ -0,0 +1,2 @@ +import unittest +from inkycal.modules import Todoist as Module diff --git a/requirements.txt b/requirements.txt index 08cf8ca..fda35a6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,5 +5,5 @@ recurring-ical-events==0.1.17b0 # parse recurring events feedparser==5.2.1 # parse RSS-feeds numpy>=1.18.2 # image pre-processing #pre-installed on Raspbian, omitting arrow>=0.15.6 # time operations -jsmin>=2.2.2 # parsing settings.jsonc file -# flask, flask-wtf \ No newline at end of file +Flask==1.1.2 # webserver +Flask-WTF==0.14.3 # webforms diff --git a/server/app/routes.py b/server/app/routes.py index 9f53ced..e02a3fd 100644 --- a/server/app/routes.py +++ b/server/app/routes.py @@ -58,7 +58,7 @@ def inkycal_config(): # display size display_size = Display.get_display_size(model) - width, height = display_size[0], display_size[1] + width, height = int(display_size[0]), int(display_size[1]) # loop over the modules, add their config data based on user selection, merge the common_settings into each module's config @@ -66,12 +66,14 @@ def inkycal_config(): conf = {} module = 'module'+str(i) if request.form.get(module) != "None": - #conf = {"position":i , "name": request.form.get(module), "height": int(request.form.get(module+'_height')), "config":{}} - conf = {"position":i , "name": request.form.get(module), "size": (width, int(height*int(request.form.get(module+'_height')) /100)), "config":{}} + + conf = {"position":i , "name": request.form.get(module), "config":{}} for modules in settings: if modules['name'] == request.form.get(module): + conf['config']['size'] = (width, int(height*int(request.form.get(module+'_height')) /100)) + # Add required fields to the config of the module in question if 'requires' in modules: for key in modules['requires']: @@ -90,12 +92,12 @@ def inkycal_config(): conf['config'][key] = None # update the config dictionary - conf.update(common_settings) + conf["config"].update(common_settings) template['modules'].append(conf) # Send the data back to the server side in json dumps and convert the response to a downloadable settings.json file try: - user_settings = json.dumps(template, indent=4).encode('utf-8') + user_settings = json.dumps(template, indent=4).replace('null', '""').encode('utf-8') response = Response(user_settings, mimetype="application/json", direct_passthrough=True) response.headers['Content-Disposition'] = 'attachment; filename=settings.json' diff --git a/server/microblog.py b/server/server.py similarity index 100% rename from server/microblog.py rename to server/server.py diff --git a/setup.py b/setup.py index c12239a..9bda96a 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup __project__ = "inkycal" -__version__ = "2.0.0beta" +__version__ = "2.0.0" __description__ = "Python3 software for syncing icalendar events, weather and news on selected E-Paper displays" __packages__ = ["inkycal"] __author__ = "aceisace" @@ -15,7 +15,8 @@ __install_requires__ = ['pyowm==2.10.0', # weather 'feedparser==5.2.1', # parse RSS-feeds 'numpy>=1.18.2', # image pre-processing 'arrow>=0.15.6', # time handling - 'jsmin>=2.2.2' # Parsing jsonc file + 'Flask==1.1.2' # webserver + 'Flask-WTF==0.14.3' # webforms ] __classifiers__ = [