From 636172f157bc347b4f7657a81f230214b94b5336 Mon Sep 17 00:00:00 2001 From: Ace Date: Sun, 29 Nov 2020 14:51:19 +0100 Subject: [PATCH 01/13] readability improvements switched from string formatting to f-strings removed some non-required validation Standardised some logging outputs better formatting of config inside tests --- inkycal/modules/ical_parser.py | 6 ++--- inkycal/modules/inkycal_agenda.py | 38 ++++++++--------------------- inkycal/modules/inkycal_calendar.py | 16 ++++++------ inkycal/modules/inkycal_feeds.py | 10 ++++---- inkycal/modules/inkycal_jokes.py | 17 ++++++++----- inkycal/modules/inkycal_stocks.py | 2 +- inkycal/modules/inkycal_todoist.py | 10 ++++---- inkycal/modules/inkycal_weather.py | 8 +++--- inkycal/modules/template.py | 8 +++--- inkycal/tests/inkycal_feeds_test.py | 10 ++------ inkycal/tests/inkycal_image_test.py | 7 ++---- inkycal/tests/inkycal_jokes_test.py | 21 +++++++++++++--- 12 files changed, 72 insertions(+), 81 deletions(-) diff --git a/inkycal/modules/ical_parser.py b/inkycal/modules/ical_parser.py index 6186e97..d074f1c 100644 --- a/inkycal/modules/ical_parser.py +++ b/inkycal/modules/ical_parser.py @@ -61,7 +61,7 @@ class iCalendar: else: ical = [auth_ical(url, username, password)] else: - raise Exception ("Input: '{}' is not a string or list!".format(url)) + raise Exception (f"Input: '{url}' is not a string or list!") def auth_ical(url, uname, passwd): @@ -89,7 +89,7 @@ class iCalendar: elif type(url) == str: ical = (Calendar.from_ical(open(path))) else: - raise Exception ("Input: '{}' is not a string or list!".format(url)) + raise Exception (f"Input: '{url}' is not a string or list!") self.icalendars += icals logger.info('loaded iCalendars from filepaths') @@ -210,4 +210,4 @@ class iCalendar: if __name__ == '__main__': - print('running {0} in standalone mode'.format(filename)) + print(f'running {filename} in standalone mode') diff --git a/inkycal/modules/inkycal_agenda.py b/inkycal/modules/inkycal_agenda.py index 169911d..f55a446 100644 --- a/inkycal/modules/inkycal_agenda.py +++ b/inkycal/modules/inkycal_agenda.py @@ -82,25 +82,7 @@ class Agenda(inkycal_module): self.timezone = get_system_tz() # give an OK message - print('{0} loaded'.format(filename)) - - def _validate(self): - """Validate module-specific parameters""" - - if not isinstance(self.date_format, str): - print('date_format has to be an arrow-compatible token') - - if not isinstance(self.time_format, str): - print('time_format has to be an arrow-compatible token') - - if not isinstance(self.language, str): - print('language has to be a string: "en" ') - - if not isinstance(self.ical_urls, list): - print('ical_urls has to be a list ["url1", "url2"] ') - - if not isinstance(self.ical_files, list): - print('ical_files has to be a list ["path1", "path2"] ') + print(f'{filename} loaded') def generate_image(self): """Generate image for this module""" @@ -110,7 +92,7 @@ class Agenda(inkycal_module): im_height = int(self.height - (2 * self.padding_top)) im_size = im_width, im_height - logger.info('Image size: {0}'.format(im_size)) + logger.info(f'Image size: {im_size}') # Create an image for black pixels and one for coloured pixels im_black = Image.new('RGB', size = im_size, color = 'white') @@ -121,7 +103,7 @@ class Agenda(inkycal_module): line_height = int(self.font.getsize('hg')[1]) + line_spacing line_width = im_width max_lines = im_height // line_height - logger.debug(('max lines:',max_lines)) + logger.debug(f'max lines: {max_lines}') # Create timeline for agenda now = arrow.now() @@ -156,11 +138,11 @@ class Agenda(inkycal_module): date_width = int(max([self.font.getsize( dates['begin'].format(self.date_format, locale=self.language))[0] for dates in agenda_events]) * 1.2) - logger.debug(('date_width:', date_width)) + logger.debug(f'date_width: {date_width}') # Calculate positions for each line line_pos = [(0, int(line * line_height)) for line in range(max_lines)] - logger.debug(('line_pos:', line_pos)) + logger.debug(f'line_pos: {line_pos}') # Check if any events were filtered if upcoming_events: @@ -170,19 +152,19 @@ class Agenda(inkycal_module): time_width = int(max([self.font.getsize( events['begin'].format(self.time_format, locale=self.language))[0] for events in upcoming_events]) * 1.2) - logger.debug(('time_width:', time_width)) + logger.debug(f'time_width: {time_width}') # Calculate x-pos for time x_time = date_width - logger.debug(('x-time:', x_time)) + logger.debug(f'x-time: {x_time}') # Find out how much space is left for event titles event_width = im_width - time_width - date_width - logger.debug(('width for events:', event_width)) + logger.debug(f'width for events: {event_width}') # Calculate x-pos for event titles x_event = date_width + time_width - logger.debug(('x-event:', x_event)) + logger.debug(f'x-event: {x_event}') # Merge list of dates and list of events agenda_events += upcoming_events @@ -247,4 +229,4 @@ class Agenda(inkycal_module): return im_black, im_colour if __name__ == '__main__': - print('running {0} in standalone mode'.format(filename)) + print(f'running {filename} in standalone mode') diff --git a/inkycal/modules/inkycal_calendar.py b/inkycal/modules/inkycal_calendar.py index e05b3ca..7f6141a 100644 --- a/inkycal/modules/inkycal_calendar.py +++ b/inkycal/modules/inkycal_calendar.py @@ -85,7 +85,7 @@ class Calendar(inkycal_module): fonts['NotoSans-SemiCondensed'], size = self.fontsize) # give an OK message - print('{0} loaded'.format(filename)) + print(f'{filename} loaded') def generate_image(self): """Generate image for this module""" @@ -95,7 +95,7 @@ class Calendar(inkycal_module): im_height = int(self.height - (2 * self.padding_top)) im_size = im_width, im_height - logger.info('Image size: {0}'.format(im_size)) + logger.info(f'Image size: {im_size}') # Create an image for black pixels and one for coloured pixels im_black = Image.new('RGB', size = im_size, color = 'white') @@ -104,8 +104,8 @@ class Calendar(inkycal_module): # Allocate space for month-names, weekdays etc. month_name_height = int(im_height * 0.1) weekdays_height = int(im_height * 0.05) - logger.debug((f"month_name_height: {month_name_height}")) - logger.debug((f"weekdays_height: {weekdays_height}")) + logger.debug(f"month_name_height: {month_name_height}") + logger.debug(f"weekdays_height: {weekdays_height}") if self.show_events == True: @@ -129,8 +129,8 @@ class Calendar(inkycal_module): x_spacing_calendar = int((im_width % calendar_cols) / 2) y_spacing_calendar = int((im_height % calendar_rows) / 2) - logger.debug((f"x_spacing_calendar: {x_spacing_calendar}")) - logger.debug((f"y_spacing_calendar :{y_spacing_calendar}")) + logger.debug(f"x_spacing_calendar: {x_spacing_calendar}") + logger.debug(f"y_spacing_calendar :{y_spacing_calendar}") # Calculate positions for days of month grid_start_y = (month_name_height + weekdays_height + y_spacing_calendar) @@ -160,7 +160,7 @@ class Calendar(inkycal_module): # Set up weeknames in local language and add to main section weekday_names = [weekstart.shift(days=+_).format('ddd',locale=self.language) for _ in range(7)] - logger.debug('weekday names: {}'.format(weekday_names)) + logger.debug(f'weekday names: {weekday_names}') for _ in range(len(weekday_pos)): write( @@ -333,4 +333,4 @@ class Calendar(inkycal_module): return im_black, im_colour if __name__ == '__main__': - print('running {0} in standalone mode'.format(filename)) + print(f'running {filename} in standalone mode') diff --git a/inkycal/modules/inkycal_feeds.py b/inkycal/modules/inkycal_feeds.py index 393252d..c312656 100644 --- a/inkycal/modules/inkycal_feeds.py +++ b/inkycal/modules/inkycal_feeds.py @@ -53,7 +53,7 @@ class Feeds(inkycal_module): # Check if all required parameters are present for param in self.requires: if not param in config: - raise Exception('config is missing {}'.format(param)) + raise Exception(f'config is missing {param}') # required parameters if config["feed_urls"] and isinstance(config['feed_urls'], str): @@ -65,7 +65,7 @@ class Feeds(inkycal_module): self.shuffle_feeds = config["shuffle_feeds"] # give an OK message - print('{0} loaded'.format(filename)) + print(f'{filename} loaded') def _validate(self): """Validate module-specific parameters""" @@ -81,7 +81,7 @@ class Feeds(inkycal_module): im_width = int(self.width - (2 * self.padding_left)) im_height = int(self.height - (2 * self.padding_top)) im_size = im_width, im_height - logger.info('image size: {} x {} px'.format(im_width, im_height)) + logger.info(f'Image size: {im_size}') # Create an image for black pixels and one for coloured pixels im_black = Image.new('RGB', size = im_size, color = 'white') @@ -111,7 +111,7 @@ class Feeds(inkycal_module): for feeds in self.feed_urls: text = feedparser.parse(feeds) for posts in text.entries: - parsed_feeds.append('•{0}: {1}'.format(posts.title, posts.summary)) + parsed_feeds.append(f'•{posts.title}: {posts.summary}') self._parsed_feeds = parsed_feeds @@ -151,4 +151,4 @@ class Feeds(inkycal_module): return im_black, im_colour if __name__ == '__main__': - print('running {0} in standalone/debug mode'.format(filename)) + print(f'running {filename} in standalone/debug mode') diff --git a/inkycal/modules/inkycal_jokes.py b/inkycal/modules/inkycal_jokes.py index 339a3da..7fb0abe 100644 --- a/inkycal/modules/inkycal_jokes.py +++ b/inkycal/modules/inkycal_jokes.py @@ -33,7 +33,7 @@ class Jokes(inkycal_module): config = config['config'] # give an OK message - print('{0} loaded'.format(filename)) + print(f'{filename} loaded') def generate_image(self): """Generate image for this module""" @@ -42,7 +42,7 @@ class Jokes(inkycal_module): im_width = int(self.width - (2 * self.padding_left)) im_height = int(self.height - (2 * self.padding_top)) im_size = im_width, im_height - logger.info('image size: {} x {} px'.format(im_width, im_height)) + logger.info(f'image size: {im_width} x {im_height} px') # Create an image for black pixels and one for coloured pixels im_black = Image.new('RGB', size = im_size, color = 'white') @@ -76,7 +76,7 @@ class Jokes(inkycal_module): header = {"accept": "text/plain"} response = requests.get(url, headers=header) response.encoding = 'utf-8' # Change encoding to UTF-8 - joke = response.text + joke = response.text.rstrip() # use to remove newlines logger.debug(f"joke: {joke}") # wrap text in case joke is too large @@ -87,13 +87,18 @@ class Jokes(inkycal_module): if len(wrapped) > max_lines: logger.error("Ohoh, Joke is too large for given space, please consider " "increasing the size for this module") - logger.error("Removing lines in reverse order") - wrapped = wrapped[:max_lines] - # Write feeds on image + # Write the joke on the image for _ in range(len(wrapped)): + if _+1 > max_lines: + logger.error('Ran out of lines for this joke :/') + break write(im_black, line_positions[_], (line_width, line_height), wrapped[_], font = self.font, alignment= 'left') # Save image of black and colour channel in image-folder return im_black, im_colour + + +if __name__ == '__main__': + print(f'running {filename} in standalone/debug mode') diff --git a/inkycal/modules/inkycal_stocks.py b/inkycal/modules/inkycal_stocks.py index 0d20ae2..9aca534 100644 --- a/inkycal/modules/inkycal_stocks.py +++ b/inkycal/modules/inkycal_stocks.py @@ -60,7 +60,7 @@ class Stocks(inkycal_module): im_width = int(self.width - (2 * self.padding_left)) im_height = int(self.height - (2 * self.padding_top)) im_size = im_width, im_height - logger.info('image size: {} x {} px'.format(im_width, im_height)) + logger.info(f'Image size: {im_size}') # Create an image for black pixels and one for coloured pixels (required) im_black = Image.new('RGB', size = im_size, color = 'white') diff --git a/inkycal/modules/inkycal_todoist.py b/inkycal/modules/inkycal_todoist.py index 116d136..c1deeb6 100644 --- a/inkycal/modules/inkycal_todoist.py +++ b/inkycal/modules/inkycal_todoist.py @@ -34,7 +34,7 @@ class Todoist(inkycal_module): optional = { 'project_filter': { "label":"Show Todos only from following project (separated by a comma). Leave empty to show "+ - "todos from all projects", + "todos from all projects", } } @@ -48,7 +48,7 @@ class Todoist(inkycal_module): # Check if all required parameters are present for param in self.requires: if not param in config: - raise Exception('config is missing {}'.format(param)) + raise Exception(f'config is missing {param}') # module specific parameters self.api_key = config['api_key'] @@ -63,7 +63,7 @@ class Todoist(inkycal_module): self._api.sync() # give an OK message - print('{0} loaded'.format(self.name)) + print(f'{filename} loaded') def _validate(self): """Validate module-specific parameters""" @@ -77,7 +77,7 @@ class Todoist(inkycal_module): im_width = int(self.width - (2 * self.padding_left)) im_height = int(self.height - (2 * self.padding_top)) im_size = im_width, im_height - logger.info('image size: {} x {} px'.format(im_width, im_height)) + logger.info(f'Image size: {im_size}') # Create an image for black pixels and one for coloured pixels im_black = Image.new('RGB', size = im_size, color = 'white') @@ -196,4 +196,4 @@ class Todoist(inkycal_module): return im_black, im_colour if __name__ == '__main__': - print('running {0} in standalone/debug mode'.format(filename)) + print(f'running {filename} in standalone/debug mode') diff --git a/inkycal/modules/inkycal_weather.py b/inkycal/modules/inkycal_weather.py index 1913fb5..6f4a178 100644 --- a/inkycal/modules/inkycal_weather.py +++ b/inkycal/modules/inkycal_weather.py @@ -90,7 +90,7 @@ class Weather(inkycal_module): # Check if all required parameters are present for param in self.requires: if not param in config: - raise Exception('config is missing {}'.format(param)) + raise Exception(f'config is missing {param}') # required parameters self.api_key = config['api_key'] @@ -146,7 +146,7 @@ class Weather(inkycal_module): im_width = int(self.width - (2 * self.padding_left)) im_height = int(self.height - (2 * self.padding_top)) im_size = im_width, im_height - logger.info('image size: {} x {} px'.format(im_width, im_height)) + logger.info(f'Image size: {im_size}') # Create an image for black pixels and one for coloured pixels im_black = Image.new('RGB', size = im_size, color = 'white') @@ -391,7 +391,7 @@ class Weather(inkycal_module): daily_temp = [round(_.temperature(unit=temp_unit)['temp'], ndigits=dec_temp) for _ in forecasts] # Calculate min. and max. temp for this day - temp_range = '{}°/{}°'.format(max(daily_temp), min(daily_temp)) + temp_range = f'{max(daily_temp)}°/{min(daily_temp)}°' # Get all weather icon codes for this day @@ -510,4 +510,4 @@ class Weather(inkycal_module): return im_black, im_colour if __name__ == '__main__': - print('running {0} in standalone mode'.format(filename)) + print(f'running {filename} in standalone mode') diff --git a/inkycal/modules/template.py b/inkycal/modules/template.py index 87ebd0d..1756268 100644 --- a/inkycal/modules/template.py +++ b/inkycal/modules/template.py @@ -44,9 +44,9 @@ class inkycal_module(metaclass=abc.ABCMeta): self.fontsize = value else: setattr(self, key, value) - print("set '{}' to '{}'".format(key,value)) + print(f"set '{key}' to '{value}'") else: - print('{0} does not exist'.format(key)) + print(f'{key} does not exist') pass # Check if validation has been implemented @@ -70,12 +70,12 @@ class inkycal_module(metaclass=abc.ABCMeta): if hasattr(cls, 'requires'): for each in cls.requires: if not "label" in cls.requires[each]: - raise Exception("no label found for {}".format(each)) + raise Exception(f"no label found for {each}") if hasattr(cls, 'optional'): for each in cls.optional: if not "label" in cls.optional[each]: - raise Exception("no label found for {}".format(each)) + raise Exception(f"no label found for {each}") conf = { "name": cls.__name__, diff --git a/inkycal/tests/inkycal_feeds_test.py b/inkycal/tests/inkycal_feeds_test.py index 7fdf216..28255c6 100644 --- a/inkycal/tests/inkycal_feeds_test.py +++ b/inkycal/tests/inkycal_feeds_test.py @@ -9,10 +9,7 @@ tests = [ "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" + "padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en" } }, { @@ -22,10 +19,7 @@ tests = [ "size": [400,100], "feed_urls": "http://feeds.bbci.co.uk/news/world/rss.xml#", "shuffle_feeds": False, - "padding_x": 10, - "padding_y": 10, - "fontsize": 12, - "language": "en" + "padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en" } }, ] diff --git a/inkycal/tests/inkycal_image_test.py b/inkycal/tests/inkycal_image_test.py index 0a99797..abedacf 100644 --- a/inkycal/tests/inkycal_image_test.py +++ b/inkycal/tests/inkycal_image_test.py @@ -10,11 +10,8 @@ tests = [ "path": "https://cdn.britannica.com/s:700x500/84/73184-004-E5A450B5/Sunflower-field-Fargo-North-Dakota.jpg", "rotation": "0", "layout": "fill", - "padding_x": 0, - "padding_y": 0, - "fontsize": 12, - "language": "en", - "colours": "bwr" + "colours": "bwr", + "padding_x": 0, "padding_y": 0, "fontsize": 12, "language": "en", } }, ] diff --git a/inkycal/tests/inkycal_jokes_test.py b/inkycal/tests/inkycal_jokes_test.py index 5967165..c653b5d 100644 --- a/inkycal/tests/inkycal_jokes_test.py +++ b/inkycal/tests/inkycal_jokes_test.py @@ -7,10 +7,23 @@ tests = [ "name": "Jokes", "config": { "size": [300, 60], - "padding_x": 10, - "padding_y": 10, - "fontsize": 12, - "language": "en" + "padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en" + } +}, +{ + "position": 1, + "name": "Jokes", + "config": { + "size": [300, 30], + "padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en" + } +}, +{ + "position": 1, + "name": "Jokes", + "config": { + "size": [100, 800], + "padding_x": 10, "padding_y": 10, "fontsize": 18, "language": "en" } }, ] From 1c851e0549867a685a37470b8526976267a0eee9 Mon Sep 17 00:00:00 2001 From: Ace Date: Sun, 29 Nov 2020 14:51:48 +0100 Subject: [PATCH 02/13] avoid pushing files from 9.7" epaper drivers --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index 7318d72..1ed19ad 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,8 @@ lib/ share/ lib64 pyvenv.cfg +*.m4 +*.h +*.guess +*.log +*.in From dc536ff63af027896917998e88530ff633c4aaca Mon Sep 17 00:00:00 2001 From: Ace Date: Sun, 29 Nov 2020 14:52:50 +0100 Subject: [PATCH 03/13] Added Slideshow module A module that cycles through images in a given folder --- inkycal/__init__.py | 5 +- inkycal/modules/__init__.py | 3 +- inkycal/modules/inkycal_slideshow.py | 129 +++++++++++++++++++++++++++ 3 files changed, 134 insertions(+), 3 deletions(-) create mode 100644 inkycal/modules/inkycal_slideshow.py diff --git a/inkycal/__init__.py b/inkycal/__init__.py index edbacf6..91d44d5 100644 --- a/inkycal/__init__.py +++ b/inkycal/__init__.py @@ -9,8 +9,9 @@ import inkycal.modules.inkycal_feeds import inkycal.modules.inkycal_todoist import inkycal.modules.inkycal_image import inkycal.modules.inkycal_jokes -import inkycal.modules.inkycal_stocks +import inkycal.modules.inkycal_slideshow # import inkycal.modules.inkycal_server # Main file -from inkycal.main import Inkycal \ No newline at end of file +from inkycal.main import Inkycal +import inkycal.modules.inkycal_stocks diff --git a/inkycal/modules/__init__.py b/inkycal/modules/__init__.py index 0a3921b..7ea2de5 100644 --- a/inkycal/modules/__init__.py +++ b/inkycal/modules/__init__.py @@ -5,5 +5,6 @@ from .inkycal_feeds import Feeds from .inkycal_todoist import Todoist from .inkycal_image import Inkyimage from .inkycal_jokes import Jokes +from .inkycal_stocks import Stocks +from .inkycal_slideshow import Slideshow #from .inkycal_server import Inkyserver -from .inkycal_stocks import Stocks \ No newline at end of file diff --git a/inkycal/modules/inkycal_slideshow.py b/inkycal/modules/inkycal_slideshow.py new file mode 100644 index 0000000..965025e --- /dev/null +++ b/inkycal/modules/inkycal_slideshow.py @@ -0,0 +1,129 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +""" +Image module for Inkycal Project +Copyright by aceisace +""" +import glob + +from inkycal.modules.template import inkycal_module +from inkycal.custom import * + +# PIL has a class named Image, use alias for Inkyimage -> Images +from inkycal.modules.inky_image import Inkyimage as Images + +filename = os.path.basename(__file__).split('.py')[0] +logger = logging.getLogger(filename) + +class Slideshow(inkycal_module): + """Cycles through images in a local image folder + """ + name = "Slideshow - cycle through images from a local folder" + + requires = { + + "path":{ + "label":"Path to a local folder, e.g. /home/pi/Desktop/images. " + "Only PNG and JPG/JPEG images are used for the slideshow." + }, + + "use_colour": { + "label":"Does the display support colour?", + "options": [True, False] + } + + } + + optional = { + + "autoflip":{ + "label":"Should the image be flipped automatically?", + "options": [True, False] + } + } + + def __init__(self, config): + """Initialize module""" + + super().__init__(config) + + config = config['config'] + + # required parameters + for param in self.requires: + if not param in config: + raise Exception(f'config is missing {param}') + + # optional parameters + self.path = config['path'] + self.use_colour = config['use_colour'] + self.autoflip = config['autoflip'] + + # Get the full path of all png/jpg/jpeg images in the given folder + all_files = glob.glob(f'{self.path}/*') + self.images = [i for i in all_files + if i.split('.')[-1].lower() in ('jpg', 'jpeg', 'png')] + + if not self.images: + logger.error('No images found in the given folder, please ' + 'double check your path!') + raise Exception('No images found in the given folder path :/') + + # set a 'first run' signal + self._first_run = True + + # give an OK message + print(f'{filename} loaded') + + def generate_image(self): + """Generate image for this module""" + + # Define new image size with respect to padding + im_width = int(self.width - (2 * self.padding_left)) + im_height = int(self.height - (2 * self.padding_top)) + im_size = im_width, im_height + + logger.info(f'Image size: {im_size}') + + # rotates list items by 1 index + def rotate(somelist): + return somelist[1:] + somelist[:1] + + # Switch to the next image if this is not the first run + if self._first_run == True: + self._first_run = False + else: + self.images = rotate(self.images) + + # initialize custom image class + im = Images() + + # use the image at the first index + im.load(self.images[0]) + + # Remove background if present + im.remove_alpha() + + # if autoflip was enabled, flip the image + if self.autoflip == True: + im.autoflip('vertical') + + # resize the image so it can fit on the epaper + im.resize( width=im_width, height=im_height ) + + # convert images according to given settings + if self.use_colour == False: + im_black = im.to_mono() + im_colour = Image.new('RGB', size = im_black.size, color = 'white') + else: + im_black, im_colour = im.to_colour() + + # with the images now send, clear the current image + im.clear() + + # return images + return im_black, im_colour + +if __name__ == '__main__': + print(f'running {filename} in standalone/debug mode') From 8a82a149af237cdcf8785787128da57dc749c390 Mon Sep 17 00:00:00 2001 From: Ace Date: Sun, 29 Nov 2020 14:53:13 +0100 Subject: [PATCH 04/13] removed a problematic line --- inkycal/custom/functions.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/inkycal/custom/functions.py b/inkycal/custom/functions.py index 3eded54..cac626d 100644 --- a/inkycal/custom/functions.py +++ b/inkycal/custom/functions.py @@ -35,8 +35,6 @@ for path,dirs,files in os.walk(fonts_location): name = filename.split('.ttf')[0] fonts[name] = os.path.join(path, filename) -# del name, filename, files - available_fonts = [key for key,values in fonts.items()] def get_fonts(): From eb806526bd73900da8197d32718a62431c09fffb Mon Sep 17 00:00:00 2001 From: Ace Date: Sun, 29 Nov 2020 14:54:00 +0100 Subject: [PATCH 05/13] Added custom image class --- inkycal/modules/inky_image.py | 246 ++++++++++++++++++++++++++++++++++ 1 file changed, 246 insertions(+) create mode 100644 inkycal/modules/inky_image.py diff --git a/inkycal/modules/inky_image.py b/inkycal/modules/inky_image.py new file mode 100644 index 0000000..257e22b --- /dev/null +++ b/inkycal/modules/inky_image.py @@ -0,0 +1,246 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +""" +Custom image class for Inkycal Project +Takes care of handling images. Made to be used by other modules to handle +images. + +Copyright by aceisace +""" + +from PIL import Image, ImageOps +import requests +import numpy +import os +import logging + +filename = os.path.basename(__file__).split('.py')[0] +logger = logging.getLogger(filename) + +class Inkyimage: + """Inkyimage class + + missing documentation, lazy devs :/ + """ + + def __init__(self, image=None): + """Initialize Inkyimage module""" + + # no image initially + self.image = image + + # give an OK message + print(f'{filename} loaded') + + def load(self, path): + """loads an image from a URL or filepath. + + Args: + - path:The full path or url of the image file + e.g. `https://sample.com/logo.png` or `/home/pi/Downloads/nice_pic.png` + + Raises: + - FileNotFoundError: This Exception is raised when the file could not be + found. + - OSError: A OSError is raised when the URL doesn't point to the correct + file-format, i.e. is not an image + - TypeError: if the URLS doesn't start with htpp + """ + # Try to open the image if it exists and is an image file + try: + if path.startswith('http'): + logger.debug('loading image from URL') + image = Image.open(requests.get(path, stream=True).raw) + else: + logger.info('loading image from local path') + image = Image.open(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(f'width: {image.width}, height: {image.height}') + + image.convert(mode='RGBA') #convert to a more suitable format + self.image = image + print('loaded Image') + + def clear(self): + """Removes currently saved image if present""" + if self.image: + self.image = None + print('cleared') + + def _preview(self): + """Preview the image on gpicview (only works on Rapsbian with Desktop)""" + if self._image_loaded(): + path = '/home/pi/Desktop/' + self.image.save(path+'temp.png') + os.system("gpicview "+path+'temp.png') + os.system('rm '+path+'temp.png') + + @staticmethod + def preview(image): + """"Previews an 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') + + def _image_loaded(self): + """returns True if image was loaded""" + if self.image: + return True + else: + print('image not loaded') + return False + + def flip(self, angle): + """Flips the image by the given angle. + + Args: + - angle:->int. A multiple of 90, e.g. 90, 180, 270, 360. + """ + if self._image_loaded(): + + image = self.image + if not angle % 90 == 0: + print('Angle must be a multiple of 90') + return + + image = image.rotate(angle, expand = True) + self.image = image + print(f'flipped image by {angle} degrees') + + def autoflip(self, layout): + """flips the image automatically to the given layout. + + Args: + - layout:-> str. Choose `horizontal` or `vertical`. + + Checks the image's width and height. + + In horizontal mode, the image is flipped if the image height is greater + than the image width. + + In vertical mode, the image is flipped if the image width is greater + than the image height. + """ + if self._image_loaded(): + + image = self.image + if layout == 'horizontal': + if (image.height > image.width): + print('image width greater than image height, flipping') + image = image.rotate(90, expand=True) + + elif layout == 'vertical': + if (image.width > image.height): + print('image width greater than image height, flipping') + image = image.rotate(90, expand=True) + else: + print('layout not supported') + return + self.image = image + + def remove_alpha(self): + """Removes transparency if image has transparency. + + Checks if an image has an alpha band and replaces the transparency with + white pixels. + """ + if self._image_loaded(): + image = self.image + + if len(image.getbands()) == 4: + print('has alpha') + logger.debug('removing transparency') + bg = Image.new('RGBA', (image.width, image.height), 'white') + im = Image.alpha_composite(bg, image) + + self.image.paste(im, (0,0)) + print('removed alpha') + + def resize(self, width=None, height=None): + """Resize an image to desired width or height""" + if self._image_loaded(): + + if width == None and height == None: + print('no height of width specified') + return + + image = self.image + + if width: + initial_width = image.width + wpercent = (width/float(image.width)) + hsize = int((float(image.height)*float(wpercent))) + image = image.resize((width, hsize), Image.ANTIALIAS) + logger.debug(f"resized image from {initial_width} to {image.width}") + self.image = image + + if height: + initial_height = image.height + hpercent = (height / float(image.height)) + wsize = int(float(image.width) * float(hpercent)) + image = image.resize((wsize, height), Image.ANTIALIAS) + logger.debug(f"resized image from {initial_height} to {image.height}") + self.image = image + + def to_mono(self): + """Converts image to pure balck-white image (1-bit). + + retrns 1-bit image + + """ + if self._image_loaded(): + image = self.image + + image = image.convert('1', dither=True) + return image + + + def to_colour(self): + """Maps image colours to 3 colours. + """ + if self._image_loaded(): + image = self.image.convert('RGB') + + # Create a simple palette + pal = [255,255,255, 0,0,0, 255,0,0, 255,255,255] + + # Map each pixel of the opened image to the Palette + palette_im = Image.new('P', (3,1)) + palette_im.putpalette(pal * 64) + quantized_im = image.quantize(palette=palette_im) + quantized_im.convert('RGB') + + # Create a buffer for coloured pixels + buffer1 = numpy.array(quantized_im.convert('RGB')) + r1,g1,b1 = buffer1[:, :, 0], buffer1[:, :, 1], buffer1[:, :, 2] + + # Create a buffer for black pixels + buffer2 = numpy.array(quantized_im.convert('RGB')) + r2,g2,b2 = buffer2[:, :, 0], buffer2[:, :, 1], buffer2[:, :, 2] + + # re-construct image from coloured-pixels buffer + 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) + + # re-construct image from black pixels buffer + buffer1[numpy.logical_and(r1 == 255, b1 == 0)] = [255,255,255] + im_black = Image.fromarray(buffer1) + + return im_black, im_colour + + +if __name__ == '__main__': + print(f'running {filename} in standalone/debug mode') + +a = Inkyimage() +a.load('https://pngimg.com/uploads/pokemon/pokemon_PNG148.png') From 9216afbea83df8e1df0296a941adb4e6553dc671 Mon Sep 17 00:00:00 2001 From: Ace Date: Sun, 29 Nov 2020 14:56:44 +0100 Subject: [PATCH 06/13] improved logging + log to file Logging is now set at two levels: logging.ERROR and more important messages are shown on the console while running inkcal. logging.DEBUG (all) messages are logged inside a log file named inkycal.log in /Inkycal/logs. Fixed an issue with the info section not updating the time correctly. --- inkycal/main.py | 42 +++++++++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/inkycal/main.py b/inkycal/main.py index 832bd8e..6a9212b 100644 --- a/inkycal/main.py +++ b/inkycal/main.py @@ -11,6 +11,7 @@ from inkycal.custom import * import os import traceback import logging +from logging.handlers import RotatingFileHandler import arrow import time import json @@ -28,18 +29,36 @@ except ImportError: 'run: pip3 install numpy \nIf you are on Raspberry Pi ' 'remove numpy: pip3 uninstall numpy \nThen try again.') -logging.basicConfig( - level = logging.INFO, #DEBUG > #INFO > #ERROR > #WARNING > #CRITICAL - format='%(name)s -> %(levelname)s -> %(asctime)s -> %(message)s', - datefmt='%d-%m-%Y %H:%M') +# (i): Logging shows logs above a threshold level. +# e.g. logging.DEBUG will show all from DEBUG until CRITICAL +# e.g. logging.ERROR will show from ERROR until CRITICAL +# #DEBUG > #INFO > #ERROR > #WARNING > #CRITICAL + +# On the console, set a logger to show only important logs +# (level ERROR or higher) +stream_handler = logging.StreamHandler() +stream_handler.setLevel(logging.ERROR) + +# Save all logs to a file, which contains much more detailed output +logging.basicConfig( + level = logging.DEBUG, + format='%(asctime)s | %(name)s | %(levelname)s: %(message)s', + datefmt='%d.%m.%Y %H:%M', + handlers=[ + + stream_handler, # add stream handler from above + + RotatingFileHandler( # log to a file too + f'{top_level}/logs/inkycal.log', # file to log + maxBytes=2097152, # 2MB max filesize + backupCount=5 # create max 5 log files + ) + ] + ) -logger = logging.getLogger('inykcal main') # TODO: fix issue with non-render mode requiring SPI -# TODO: fix info section not updating after a calibration -# TODO: add function to add/remove third party modules # TODO: autostart -> supervisor? -# TODO: logging to files class Inkycal: """Inkycal main class @@ -129,7 +148,7 @@ class Inkycal: # If a module was not found, print an error message except ImportError: - print('Could not find module: "{module}". Please try to import manually') + print(f'Could not find module: "{module}". Please try to import manually') # If something unexpected happened, show the error message except Exception as e: @@ -236,7 +255,7 @@ class Inkycal: errors = [] # store module numbers in here # short info for info-section - self.info = f"{runtime.format('D MMM @ HH:mm')} " + self.info = f"{arrow.now(tz=get_system_tz()).format('D MMM @ HH:mm')} " for number in range(1, self._module_number): @@ -257,6 +276,7 @@ class Inkycal: print('Error!') print(traceback.format_exc()) self.info += f"module {number}: Error! " + logging.error(f'Exception in module {number}:', exc_info=True) if errors: print('Error/s in modules:',*errors) @@ -657,4 +677,4 @@ class Inkycal: print(f"Your module '{filename}' with class '{classname}' was removed.") if __name__ == '__main__': - print('running {0} in standalone/debug mode'.format('inkycal main')) + print(f'running inkycal main in standalone/debug mode') From f0035bf9225dc5c3b001a472378b1190fcac2c33 Mon Sep 17 00:00:00 2001 From: Ace Date: Sun, 29 Nov 2020 14:58:13 +0100 Subject: [PATCH 07/13] created new logs folder --- logs/dummy.txt.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 logs/dummy.txt.txt diff --git a/logs/dummy.txt.txt b/logs/dummy.txt.txt new file mode 100644 index 0000000..e69de29 From 301d239c3f7b77dbaa79ec2ba16ee55ede399870 Mon Sep 17 00:00:00 2001 From: Ace Date: Sun, 29 Nov 2020 23:43:56 +0100 Subject: [PATCH 08/13] fixed calibration on 9.7" E-Paper displays --- inkycal/display/display.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/inkycal/display/display.py b/inkycal/display/display.py index ff53476..8cd0a2e 100644 --- a/inkycal/display/display.py +++ b/inkycal/display/display.py @@ -123,8 +123,10 @@ class Display: epaper = self._epaper epaper.init() - white = Image.new('1', (epaper.width, epaper.height), 'white') - black = Image.new('1', (epaper.width, epaper.height), 'black') + display_size = self.get_display_size(self.model_name) + + white = Image.new('1', display_size, 'white') + black = Image.new('1', display_size, 'black') print('----------Started calibration of ePaper display----------') if self.supports_colour == True: @@ -136,7 +138,7 @@ class Display: epaper.display(epaper.getbuffer(white), epaper.getbuffer(black)) print('white...') epaper.display(epaper.getbuffer(white), epaper.getbuffer(white)) - print('Cycle {0} of {1} complete'.format(_+1, cycles)) + print(f'Cycle {_+1} of {cycles} complete') if self.supports_colour == False: for _ in range(cycles): @@ -145,7 +147,7 @@ class Display: epaper.display(epaper.getbuffer(black)) print('white...') epaper.display(epaper.getbuffer(white)), - print('Cycle {0} of {1} complete'.format(_+1, cycles)) + print(f'Cycle {_+1} of {cycles} complete') print('-----------Calibration complete----------') epaper.sleep() From 6d2c289e76d01ba0bee0076c85845abcaca5c79a Mon Sep 17 00:00:00 2001 From: Ace Date: Sun, 29 Nov 2020 23:44:21 +0100 Subject: [PATCH 09/13] Fixed loading iCalendars from filepath --- inkycal/modules/ical_parser.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/inkycal/modules/ical_parser.py b/inkycal/modules/ical_parser.py index d074f1c..05fb46d 100644 --- a/inkycal/modules/ical_parser.py +++ b/inkycal/modules/ical_parser.py @@ -84,14 +84,14 @@ class iCalendar: example: 'path1' (single file) OR ['path1', 'path2'] (multiple files) returns a list of iCalendars as string (raw) """ - if type(url) == list: + if type(filepath) == list: ical = (Calendar.from_ical(open(path)) for path in filepath) - elif type(url) == str: + elif type(filepath) == str: ical = (Calendar.from_ical(open(path))) else: - raise Exception (f"Input: '{url}' is not a string or list!") + raise Exception (f"Input: '{filepath}' is not a string or list!") - self.icalendars += icals + self.icalendars += ical logger.info('loaded iCalendars from filepaths') def get_events(self, timeline_start, timeline_end, timezone=None): From 3d4e24eee17303ba7a624b1d0f37d8157266f780 Mon Sep 17 00:00:00 2001 From: Ace Date: Sun, 29 Nov 2020 23:45:17 +0100 Subject: [PATCH 10/13] Adapted Inkycal_image By using a helper class, this module could be simplified greatly --- inkycal/modules/inkycal_image.py | 318 ++++++------------------------- 1 file changed, 54 insertions(+), 264 deletions(-) diff --git a/inkycal/modules/inkycal_image.py b/inkycal/modules/inkycal_image.py index 8e346a5..040606f 100644 --- a/inkycal/modules/inkycal_image.py +++ b/inkycal/modules/inkycal_image.py @@ -9,55 +9,46 @@ Copyright by aceisace from inkycal.modules.template import inkycal_module from inkycal.custom import * -from PIL import ImageOps -import requests -import numpy +from inkycal.modules.inky_image import Inkyimage as Images filename = os.path.basename(__file__).split('.py')[0] logger = logging.getLogger(filename) class Inkyimage(inkycal_module): - """Image class - display an image from a given path or URL + """Displays an image from URL or local path """ name = "Inykcal Image - show an image from a URL or local path" requires = { - 'path': { - "label":"Please enter the full path of the image file (local or URL)", - } + + "path":{ + "label":"Path to a local folder, e.g. /home/pi/Desktop/images. " + "Only PNG and JPG/JPEG images are used for the slideshow." + }, - } + "use_colour": { + "label":"Does the display support colour?", + "options": [True, False] + } + + } optional = { + + "autoflip":{ + "label":"Should the image be flipped automatically?", + "options": [True, False] + }, - '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" - }, - - 'colours':{ - "label":"Specify the colours of your panel. Choose between bw (black and white), bwr (black, white and red) or bwy (black, white and yellow)", - "options": ['bw', 'bwr', 'bwy'], - "default": "bw" + "orientation":{ + "label": "Please select the desired orientation", + "options": ["vertical", "horizontal"] + } } - } - - - # TODO: thorough testing and code cleanup - # TODO: presentation mode (cycle through images in folder) - def __init__(self, config): - """Initialize inkycal_rss module""" + """Initialize module""" super().__init__(config) @@ -66,257 +57,56 @@ class Inkyimage(inkycal_module): # required parameters for param in self.requires: if not param in config: - raise Exception('config is missing {}'.format(param)) + raise Exception(f'config is missing {param}') # optional parameters - self.image_path = self.config['path'] - - self.rotation = self.config['rotation'] - self.layout = self.config['layout'] - self.colours = self.config['colours'] + self.path = config['path'] + self.use_colour = config['use_colour'] + self.autoflip = config['autoflip'] + self.orientation = config['orientation'] # give an OK message - print('{0} loaded'.format(self.name)) + print(f'{filename} loaded') - 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_width = int(self.width - (2 * self.padding_left)) + im_height = int(self.height - (2 * self.padding_top)) im_size = im_width, im_height - logger.info('image size: {} x {} px'.format(im_width, im_height)) - logger.info('image path: {}'.format(self.image_path)) - logger.info('colors: {}'.format(self.colours)) - # 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.info(f'Image size: {im_size}') - logger.debug(('image-width:', self.image.width)) - logger.debug(('image-height:', self.image.height)) + # initialize custom image class + im = Images() - # 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') + # use the image at the first index + im.load(self.path) - # do the required operations - self._remove_alpha() - self._to_layout() - black, colour = self._map_colours(self.colours) + # Remove background if present + im.remove_alpha() - # paste the images on the canvas - im_black.paste(black, (self.x, self.y)) - im_colour.paste(colour, (self.x, self.y)) + # if autoflip was enabled, flip the image + if self.autoflip == True: + im.autoflip(self.orientation) - # 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') + # resize the image so it can fit on the epaper + im.resize( width=im_width, height=im_height ) + + # convert images according to given settings + if self.use_colour == False: + im_black = im.to_mono() + im_colour = Image.new('RGB', size = im_black.size, color = 'white') + else: + im_black, im_colour = im.to_colour() + + # with the images now send, clear the current image + im.clear() # return images - return black, colour - - 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] - else: - logger.info('Unrecognized colors: {}, falling back to black and white'.format(colours)) - # Fallback to black-white images, use monochrome dithering - im_black = im.convert('1', dither=True) - im_colour = None - - # 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)) + print(f'running {filename} in standalone/debug mode') From 031b3211ec71fe0ab3fa037e2fb86450b0db97b0 Mon Sep 17 00:00:00 2001 From: Ace Date: Sun, 29 Nov 2020 23:45:48 +0100 Subject: [PATCH 11/13] added orientation option for slideshow --- inkycal/modules/inkycal_slideshow.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/inkycal/modules/inkycal_slideshow.py b/inkycal/modules/inkycal_slideshow.py index 965025e..89f215d 100644 --- a/inkycal/modules/inkycal_slideshow.py +++ b/inkycal/modules/inkycal_slideshow.py @@ -40,7 +40,12 @@ class Slideshow(inkycal_module): "autoflip":{ "label":"Should the image be flipped automatically?", "options": [True, False] - } + }, + + "orientation":{ + "label": "Please select the desired orientation", + "options": ["vertical", "horizontal"] + } } def __init__(self, config): @@ -59,6 +64,7 @@ class Slideshow(inkycal_module): self.path = config['path'] self.use_colour = config['use_colour'] self.autoflip = config['autoflip'] + self.orientation = config['orientation'] # Get the full path of all png/jpg/jpeg images in the given folder all_files = glob.glob(f'{self.path}/*') @@ -107,7 +113,7 @@ class Slideshow(inkycal_module): # if autoflip was enabled, flip the image if self.autoflip == True: - im.autoflip('vertical') + im.autoflip(self.orientation) # resize the image so it can fit on the epaper im.resize( width=im_width, height=im_height ) From b8ef99d07bdf9ff2500cc2148490585138b68b7b Mon Sep 17 00:00:00 2001 From: Ace Date: Sun, 29 Nov 2020 23:47:14 +0100 Subject: [PATCH 12/13] Use language from config instead of system language This fixes an issue where the weekday would be named according to the system language, but not the specified language in that module's settings. --- inkycal/modules/inkycal_weather.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/inkycal/modules/inkycal_weather.py b/inkycal/modules/inkycal_weather.py index 6f4a178..1464dc5 100644 --- a/inkycal/modules/inkycal_weather.py +++ b/inkycal/modules/inkycal_weather.py @@ -107,7 +107,7 @@ class Weather(inkycal_module): # additional configuration self.owm = OWM(self.api_key).weather_manager() self.timezone = get_system_tz() - self.locale = sys_locale()[0] + self.locale = config['language'] self.weatherfont = ImageFont.truetype( fonts['weathericons-regular-webfont'], size = self.fontsize) @@ -417,7 +417,9 @@ class Weather(inkycal_module): logger.debug((key,val)) # Get some current weather details - temperature = '{}°'.format(weather.temperature(unit=temp_unit)['temp']) + temperature = '{}°'.format(round( + weather.temperature(unit=temp_unit)['temp'], ndigits=dec_temp)) + weather_icon = weather.weather_icon_name humidity = str(weather.humidity) sunrise_raw = arrow.get(weather.sunrise_time()).to(self.timezone) From 1016aa288924175e1c953bc83da0aee4de39df36 Mon Sep 17 00:00:00 2001 From: Ace Date: Sun, 29 Nov 2020 23:51:04 +0100 Subject: [PATCH 13/13] Adapted tests for Image and Slideshow module --- inkycal/tests/inkycal_image_test.py | 89 +++++++++++++++++-- inkycal/tests/inkycal_slideshow_test.py | 108 ++++++++++++++++++++++++ 2 files changed, 190 insertions(+), 7 deletions(-) create mode 100644 inkycal/tests/inkycal_slideshow_test.py diff --git a/inkycal/tests/inkycal_image_test.py b/inkycal/tests/inkycal_image_test.py index abedacf..87cc987 100644 --- a/inkycal/tests/inkycal_image_test.py +++ b/inkycal/tests/inkycal_image_test.py @@ -1,18 +1,93 @@ import unittest from inkycal.modules import Inkyimage as Module +from inkycal.custom import top_level + +test_path = f'{top_level}/Gallery/coffee.png' tests = [ { "position": 1, "name": "Inkyimage", "config": { - "size": [528,880], - "path": "https://cdn.britannica.com/s:700x500/84/73184-004-E5A450B5/Sunflower-field-Fargo-North-Dakota.jpg", - "rotation": "0", - "layout": "fill", - "colours": "bwr", - "padding_x": 0, "padding_y": 0, "fontsize": 12, "language": "en", - } + "size": [400,200], + "path": test_path, + "use_colour": True, + "autoflip": True, + "orientation": "vertical", + "padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en" + } +}, +{ + "position": 1, + "name": "Inkyimage", + "config": { + "size": [800,500], + "path": test_path, + "use_colour": False, + "autoflip": True, + "orientation": "vertical", + "padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en" + } +}, +{ + "position": 1, + "name": "Inkyimage", + "config": { + "size": [400,100], + "path": test_path, + "use_colour": True, + "autoflip": False, + "orientation": "vertical", + "padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en" + } +}, +{ + "position": 1, + "name": "Inkyimage", + "config": { + "size": [400,100], + "path": test_path, + "use_colour": True, + "autoflip": True, + "orientation": "vertical", + "padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en" + } +}, +{ + "position": 1, + "name": "Inkyimage", + "config": { + "size": [400,100], + "path": test_path, + "use_colour": True, + "autoflip": True, + "orientation": "horizontal", + "padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en" + } +}, +{ + "position": 1, + "name": "Inkyimage", + "config": { + "size": [500, 800], + "path": test_path, + "use_colour": True, + "autoflip": True, + "orientation": "vertical", + "padding_x": 0, "padding_y": 0, "fontsize": 12, "language": "en" + } +}, +{ + "position": 1, + "name": "Inkyimage", + "config": { + "size": [500, 800], + "path": test_path, + "use_colour": True, + "autoflip": True, + "orientation": "vertical", + "padding_x": 20, "padding_y": 20, "fontsize": 12, "language": "en" + } }, ] diff --git a/inkycal/tests/inkycal_slideshow_test.py b/inkycal/tests/inkycal_slideshow_test.py new file mode 100644 index 0000000..b03433f --- /dev/null +++ b/inkycal/tests/inkycal_slideshow_test.py @@ -0,0 +1,108 @@ +import unittest +from inkycal.modules import Slideshow as Module +from inkycal.custom import top_level + +test_path = f'{top_level}/Gallery' + +tests = [ +{ + "position": 1, + "name": "Slideshow", + "config": { + "size": [400,200], + "path": test_path, + "use_colour": True, + "autoflip": True, + "orientation": "vertical", + "padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en" + } +}, +{ + "position": 1, + "name": "Slideshow", + "config": { + "size": [800,500], + "path": test_path, + "use_colour": False, + "autoflip": True, + "orientation": "vertical", + "padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en" + } +}, +{ + "position": 1, + "name": "Slideshow", + "config": { + "size": [400,100], + "path": test_path, + "use_colour": True, + "autoflip": False, + "orientation": "vertical", + "padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en" + } +}, +{ + "position": 1, + "name": "Slideshow", + "config": { + "size": [400,100], + "path": test_path, + "use_colour": True, + "autoflip": True, + "orientation": "vertical", + "padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en" + } +}, +{ + "position": 1, + "name": "Slideshow", + "config": { + "size": [400,100], + "path": test_path, + "use_colour": True, + "autoflip": True, + "orientation": "horizontal", + "padding_x": 10, "padding_y": 10, "fontsize": 12, "language": "en" + } +}, +{ + "position": 1, + "name": "Slideshow", + "config": { + "size": [500, 800], + "path": test_path, + "use_colour": True, + "autoflip": True, + "orientation": "vertical", + "padding_x": 0, "padding_y": 0, "fontsize": 12, "language": "en" + } +}, +{ + "position": 1, + "name": "Slideshow", + "config": { + "size": [500, 800], + "path": test_path, + "use_colour": True, + "autoflip": True, + "orientation": "vertical", + "padding_x": 20, "padding_y": 20, "fontsize": 12, "language": "en" + } +}, +] + +class module_test(unittest.TestCase): + def test_get_config(self): + print('getting data for web-ui...', end = "") + Module.get_config() + print('OK') + + def test_generate_image(self): + for test in tests: + print(f'test {tests.index(test)+1} generating image..') + module = Module(test) + module.generate_image() + print('OK') + +if __name__ == '__main__': + unittest.main()