diff --git a/dev_tests.py b/dev_tests.py new file mode 100644 index 0000000..9155564 --- /dev/null +++ b/dev_tests.py @@ -0,0 +1,26 @@ +from inkycal.modules.inkycal_rss import rss +from inkycal.modules.inkycal_calendar import calendar +from inkycal.modules.inkycal_agenda import agenda + +# Test rss module: +rss_size = (384, 160) +rss_config = {'rss_urls': ['http://feeds.bbci.co.uk/news/world/rss.xml#']} +rss = rss(rss_size, rss_config) +rss.generate_image() + + + +# Test calendar module: + +calendar_size = (400, 520) +calendar_config = {'week_starts_on': 'Monday', 'ical_urls': ['https://calendar.google.com/calendar/ical/en.usa%23holiday%40group.v.calendar.google.com/public/basic.ics']} +calendar = calendar(calendar_size, calendar_config) +calendar.generate_image() + + +# Test agenda module: + +agenda_size = (400, 520) +agenda_config = {'week_starts_on': 'Monday', 'ical_urls': ['https://calendar.google.com/calendar/ical/en.usa%23holiday%40group.v.calendar.google.com/public/basic.ics']} +agenda = agenda(agenda_size, agenda_config) +agenda.generate_image() diff --git a/inkycal/modules/inkycal_agenda.py b/inkycal/modules/inkycal_agenda.py index e7f511f..f08697c 100644 --- a/inkycal/modules/inkycal_agenda.py +++ b/inkycal/modules/inkycal_agenda.py @@ -8,7 +8,7 @@ Copyright by aceisace from inkycal.custom import * import calendar as cal import arrow -from ical_parser import icalendar +from inkycal.modules.ical_parser import icalendar size = (400, 520) config = {'week_starts_on': 'Monday', 'ical_urls': ['https://calendar.google.com/calendar/ical/en.usa%23holiday%40group.v.calendar.google.com/public/basic.ics']} diff --git a/inkycal/modules/inkycal_calendar.py b/inkycal/modules/inkycal_calendar.py index f5bfe89..b790dc6 100644 --- a/inkycal/modules/inkycal_calendar.py +++ b/inkycal/modules/inkycal_calendar.py @@ -26,15 +26,14 @@ class calendar: self.name = os.path.basename(__file__).split('.py')[0] self.config = section_config self.width, self.height = section_size - - self.background_colour = 'white' - self.font_colour = 'black' self.fontsize = 12 self.font = ImageFont.truetype( fonts['NotoSans-SemiCondensed'], size = self.fontsize) self.padding_x = 0.02 self.padding_y = 0.05 + self.num_font = ImageFont.truetype( + fonts['NotoSans-SemiCondensed'], size = self.fontsize) self.weekstart = 'Monday' self.show_events = True self.date_format = 'D MMM' # used for dates @@ -42,9 +41,7 @@ class calendar: self.language = 'en' # Grab from settings file? self.timezone = get_system_tz() - # urls of icalendars self.ical_urls = config['ical_urls'] - # filepaths of icalendar files self.ical_files = [] print('{0} loaded'.format(self.name)) @@ -84,7 +81,7 @@ class calendar: logging.info('Image size: {0}'.format(im_size)) # Create an image for black pixels and one for coloured pixels - im_black = Image.new('RGB', size = im_size, color = self.background_colour) + im_black = Image.new('RGB', size = im_size, color = 'white') im_colour = Image.new('RGB', size = im_size, color = 'white') # Allocate space for month-names, weekdays etc. @@ -105,11 +102,11 @@ class calendar: # Create grid and calculate icon sizes calendar_rows, calendar_cols = 6, 7 - icon_width = self.width // calendar_cols + icon_width = im_width // calendar_cols icon_height = calendar_height // calendar_rows # Calculate spacings for calendar area - x_spacing_calendar = int((im_width % icon_width) / 2) + x_spacing_calendar = int((im_width % calendar_cols) / 2) y_spacing_calendar = int((im_height % calendar_rows) / 2) # Calculate positions for days of month @@ -122,7 +119,6 @@ class calendar: weekday_pos = [(grid_start_x + icon_width*_, month_name_height) for _ in range(calendar_cols)] - now = arrow.now(tz = self.timezone) # Set weekstart of calendar to specified weekstart @@ -169,28 +165,27 @@ class calendar: grid[i], (icon_width,icon_height), str(calendar_flat[i]), - font = self.font, + font = self.num_font, fill_height = 0.5 ) # Draw a red/black circle with the current day of month in white icon = Image.new('RGBA', (icon_width, icon_height)) current_day_pos = grid[calendar_flat.index(now.day)] x_circle,y_circle = int(icon_width/2), int(icon_height/2) - radius = int(icon_width * 0.25) - text_width, text_height = self.font.getsize(str(now.day)) - x_text = int((icon_width / 2) - (text_width / 2)) - y_text = int((icon_height / 2) - (text_height / 1.7)) - ImageDraw.Draw(icon).ellipse((x_circle-radius, y_circle-radius, - x_circle+radius, y_circle+radius), fill= 'black', outline=None) - ImageDraw.Draw(icon).text((x_text, y_text), str(now.day), fill='white', - font=self.font) + radius = int(icon_width * 0.3) + ImageDraw.Draw(icon).ellipse( + (x_circle-radius, y_circle-radius, x_circle+radius, y_circle+radius), + fill= 'black', outline=None) + write(icon, (0,0), (icon_width, icon_height), str(now.day), + font=self.num_font, fill_height = 0.5, colour='white') im_colour.paste(icon, current_day_pos, icon) + # If events should be loaded and shown... if self.show_events == True: # import the ical-parser - from ical_parser import icalendar + from inkycal.modules.ical_parser import icalendar # find out how many lines can fit at max in the event section line_spacing = 0 @@ -215,7 +210,7 @@ class calendar: # Filter events for full month (even past ones) for drawing event icons month_events = parser.get_events(month_start, month_end) parser.sort() - # parser.show_events() # uncomment to show events + self.month_events = month_events # find out on which days of this month events are taking place days_with_events = [int(events['begin'].format('D')) for events in @@ -223,12 +218,7 @@ class calendar: # remove duplicates (more than one event in a single day) list(set(days_with_events)).sort() - print('days with events:', days_with_events) - -## # calculate sizes for event-markers -## square_size = int(icon_width * 0.6) -## center_x = int((icon_width - square_size) / 2) -## center_y = int((icon_height - square_size) / 2) + self._days_with_events = days_with_events # Draw a border with specified parameters around days with events for days in days_with_events: @@ -236,23 +226,15 @@ class calendar: im_colour, grid[calendar_flat.index(days)], (icon_width, icon_height), - radius = 4, + radius = 6, thickness= 1, shrinkage = (0.4, 0.4) ) - -## -## draw_square((int(grid[calendar_flat.index(days)][0]+center_x), -## int(grid[calendar_flat.index(days)][1] + center_y )), -## 8, square_size , square_size, colour='black') - - - # Filter upcoming events until 4 weeks in the future parser.clear_events() upcoming_events = parser.get_events(now, now.shift(weeks=4)) - parser.show_events() + self._upcoming_events = upcoming_events # delete events which won't be able to fit (more events than lines) upcoming_events[max_event_lines:] @@ -313,11 +295,6 @@ class calendar: (im_width, self.font.getsize(symbol)[1]), symbol, font = self.font) - -################################################################### -## Exception handling -################################################################# - # Save image of black and colour channel in image-folder im_black.save(images+self.name+'.png') im_colour.save(images+self.name+'_colour.png') @@ -325,3 +302,6 @@ class calendar: if __name__ == '__main__': print('running {0} in standalone mode'.format( os.path.basename(__file__).split('.py')[0])) + +##a = calendar(size, config) +##a.generate_image() diff --git a/inkycal/modules/inkycal_image.py b/inkycal/modules/inkycal_image.py new file mode 100644 index 0000000..43b8a57 --- /dev/null +++ b/inkycal/modules/inkycal_image.py @@ -0,0 +1,179 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +""" +Image module for inkycal Project +Copyright by aceisace +Development satge: Beta +""" + +from os import path +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? +"""----------------------------------------------------------------""" + +# First determine dimensions +if mode == 'horizontal': + display_width, display_height == display_height, display_width + +if mode == 'vertical': + raise NotImplementedError('Vertical mode is not currenctly supported') + +# .. Then substitute possibly parameterized path +# TODO Get (assigned) panel dimensions instead of display dimensions +path = path.replace('{model}', model).replace('{width}',str(display_width)).replace('{height}',str(display_height)) +print(path) + +"""Try to open the image if it exists and is an image file""" +try: + if 'http' in path: + if path_body is None: + # Plain GET + im = Image.open(requests.get(path, stream=True).raw) + else: + # POST request, passing path_body in the body + im = Image.open(requests.post(path, json=path_body, stream=True).raw) + else: + im = Image.open(path) +except FileNotFoundError: + print('Your file could not be found. Please check the path to your file.') + raise +except OSError: + print('Please check if the path points to an image file.') + raise + +"""Turn image upside-down if specified""" +if upside_down == True: + im.rotate(180, expand = True) + +if mode == 'auto': + if (im.width > im.height) and (display_width < display_height): + print('display vertical, image horizontal -> flipping image') + im = im.rotate(90, expand=True) + if (im.width < im.height) and (display_width > display_height): + print('display horizontal, image vertical -> flipping image') + im = im.rotate(90, expand=True) + +def fit_width(image, width): + """Resize an image to desired width""" + print('resizing width from', image.width, 'to', end = ' ') + wpercent = (display_width/float(image.width)) + hsize = int((float(image.height)*float(wpercent))) + img = image.resize((width, hsize), Image.ANTIALIAS) + print(img.width) + return img + +def fit_height(image, height): + """Resize an image to desired height""" + print('resizing height from', image.height, 'to', end = ' ') + hpercent = (height / float(image.height)) + wsize = int(float(image.width) * float(hpercent)) + img = image.resize((wsize, height), Image.ANTIALIAS) + print(img.height) + return img + +if im.width > display_width: + im = fit_width(im, display_width) +if im.height > display_height: + im = fit_height(im, display_height) + +if alignment == 'center': + x,y = int((display_width-im.width)/2), int((display_height-im.height)/2) +elif alignment == 'center_right': + x, y = display_width-im.width, int((display_height-im.height)/2) +elif alignment == 'center_left': + x, y = 0, int((display_height-im.height)/2) + +elif alignment == 'top_center': + x, y = int((display_width-im.width)/2), 0 +elif alignment == 'top_right': + x, y = display_width-im.width, 0 +elif alignment == 'top_left': + x, y = 0, 0 + +elif alignment == 'bottom_center': + x, y = int((display_width-im.width)/2), display_height-im.height +elif alignment == 'bottom_right': + x, y = display_width-im.width, display_height-im.height +elif alignment == 'bottom_left': + x, y = display_width-im.width, display_height-im.height + +if len(im.getbands()) == 4: + print('removing transparency') + bg = Image.new('RGBA', (im.width, im.height), 'white') + im = Image.alpha_composite(bg, im) + +image.paste(im, (x,y)) +im = image + +if colours == 'bw': + """For black-white images, use monochrome dithering""" + black = im.convert('1', dither=True) +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 != 'bw': + 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 + colour = Image.fromarray(buffer2) + """Create image for only black pixels""" + buffer1[numpy.logical_and(r1 == 255, b1 == 0)] = [255,255,255] + 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 + colour = Image.fromarray(buffer2) + """Create image for only black pixels""" + buffer1[numpy.logical_and(g1 == 255, b1 == 0)] = [255,255,255] + black = Image.fromarray(buffer1) +## +##if render == True: +## epaper = driver.EPD() +## print('Initialising E-Paper...', end = '') +## epaper.init() +## print('Done') +## +## print('Sending image data and refreshing display...', end='') +## if three_colour_support == True: +## epaper.display(epaper.getbuffer(black), epaper.getbuffer(colour)) +## else: +## epaper.display(epaper.getbuffer(black)) +## print('Done') +## +## print('Sending E-Paper to deep sleep...', end = '') +## epaper.sleep() +print('Done') diff --git a/inkycal/modules/inkycal_rss.py b/inkycal/modules/inkycal_rss.py index bc000d1..6c36275 100644 --- a/inkycal/modules/inkycal_rss.py +++ b/inkycal/modules/inkycal_rss.py @@ -18,7 +18,8 @@ except ImportError: # Debug Data (not for production use!) size = (384, 160) config = {'rss_urls': ['http://feeds.bbci.co.uk/news/world/rss.xml#']} - +#config = {'rss_urls': ['http://www.tagesschau.de/xml/atom/']} +#https://www.tagesschau.de/xml/rss2 class rss: """RSS class @@ -34,14 +35,15 @@ class rss: self.name = os.path.basename(__file__).split('.py')[0] self.config = section_config self.width, self.height = section_size - - self.background_colour = 'white' - self.font_colour = 'black' self.fontsize = 12 - self.font = ImageFont.truetype(fonts['NotoSans-SemiCondensed'], - size = self.fontsize) self.padding_x = 0.02 self.padding_y = 0.05 + self.font = ImageFont.truetype(fonts['NotoSans-SemiCondensed'], + size = self.fontsize) + + # module specifc config + self.shuffle_feeds = True + print('{0} loaded'.format(self.name)) def set(self, **kwargs): @@ -76,15 +78,14 @@ class rss: logging.info('image size: {} x {} px'.format(im_width, im_height)) # Create an image for black pixels and one for coloured pixels - im_black = Image.new('RGB', size = im_size, color = self.background_colour) + im_black = Image.new('RGB', size = im_size, color = 'white') im_colour = Image.new('RGB', size = im_size, color = 'white') # Check if internet is available if internet_available() == True: logging.info('Connection test passed') else: - logging.error('Network could not be reached :/') - raise Exception('Network could not be reached :(') + raise Exception('Network could not be reached :/') # Set some parameters for formatting rss feeds @@ -101,46 +102,48 @@ class rss: (0, spacing_top + _ * line_height ) for _ in range(max_lines)] - try: - # Create list containing all rss-feeds from all rss-feed urls - parsed_feeds = [] - for feeds in self.config['rss_urls']: - text = feedparser.parse(feeds) - for posts in text.entries: - parsed_feeds.append('•{0}: {1}'.format(posts.title, posts.summary)) - # print(parsed_feeds) - # Shuffle the list to prevent showing the same content + # Create list containing all rss-feeds from all rss-feed urls + parsed_feeds = [] + for feeds in self.config['rss_urls']: + text = feedparser.parse(feeds) + for posts in text.entries: + parsed_feeds.append('•{0}: {1}'.format(posts.title, posts.summary)) + + self._parsed_feeds = parsed_feeds + + # Shuffle the list to prevent showing the same content + if self.shuffle_feeds == True: shuffle(parsed_feeds) - # Trim down the list to the max number of lines - del parsed_feeds[max_lines:] + # Trim down the list to the max number of lines + del parsed_feeds[max_lines:] + # Wrap long text from feeds (line-breaking) + flatten = lambda z: [x for y in z for x in y] + filtered_feeds, counter = [], 0 - # Wrap long text from feeds (line-breaking) - flatten = lambda z: [x for y in z for x in y] - filtered_feeds, counter = [], 0 - - for posts in parsed_feeds: - wrapped = text_wrap(posts, font = self.font, max_width = line_width) - counter += len(filtered_feeds) + len(wrapped) - if counter < max_lines: - filtered_feeds.append(wrapped) - filtered_feeds = flatten(filtered_feeds) + for posts in parsed_feeds: + wrapped = text_wrap(posts, font = self.font, max_width = line_width) + counter += len(wrapped) + if counter < max_lines: + filtered_feeds.append(wrapped) + filtered_feeds = flatten(filtered_feeds) + self._filtered_feeds = filtered_feeds + # Check if feeds could be parsed and can be displayed + if len(filtered_feeds) == 0 and len(parsed_feeds) > 0: + print('Feeds could be parsed, but the text is too long to be displayed:/') + elif len(filtered_feeds) == 0 and len(parsed_feeds) == 0: + print('No feeds could be parsed :/') + else: # Write rss-feeds on image - """Write the correctly formatted text on the display""" for _ in range(len(filtered_feeds)): write(im_black, line_positions[_], (line_width, line_height), filtered_feeds[_], font = self.font, alignment= 'left') - # Cleanup - del filtered_feeds, parsed_feeds, wrapped, counter, text - - except Exception as e: - print('Error in {0}'.format(self.name)) - print('Reason: ',e) - write(im_black, (0,0), (im_width, im_height), str(e), font = self.font) + # Cleanup + del filtered_feeds, parsed_feeds, wrapped, counter, text # Save image of black and colour channel in image-folder im_black.save(images+self.name+'.png', 'PNG') @@ -149,3 +152,6 @@ class rss: if __name__ == '__main__': print('running {0} in standalone mode'.format( os.path.basename(__file__).split('.py')[0])) + +##a = rss(size, config) +##a.generate_image()