Merge pull request #145 from aceisace/feature/images_refactoring

Feature/images refactoring
This commit is contained in:
Ace 2020-11-29 23:58:51 +01:00 committed by GitHub
commit 2f2011583c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 749 additions and 375 deletions

5
.gitignore vendored
View File

@ -10,3 +10,8 @@ lib/
share/
lib64
pyvenv.cfg
*.m4
*.h
*.guess
*.log
*.in

View File

@ -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
from inkycal.main import Inkycal
import inkycal.modules.inkycal_stocks

View File

@ -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():

View File

@ -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()

View File

@ -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')

View File

@ -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

View File

@ -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):
@ -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 ("Input: '{}' is not a string or list!".format(url))
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):
@ -210,4 +210,4 @@ class iCalendar:
if __name__ == '__main__':
print('running {0} in standalone mode'.format(filename))
print(f'running {filename} in standalone mode')

View File

@ -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')

View File

@ -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')

View File

@ -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')

View File

@ -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')

View File

@ -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')

View File

@ -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')

View File

@ -0,0 +1,135 @@
#!/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]
},
"orientation":{
"label": "Please select the desired orientation",
"options": ["vertical", "horizontal"]
}
}
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']
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}/*')
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(self.orientation)
# 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')

View File

@ -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')

View File

@ -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')

View File

@ -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']
@ -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)
@ -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
@ -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)
@ -510,4 +512,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')

View File

@ -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__,

View File

@ -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"
}
},
]

View File

@ -1,21 +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",
"padding_x": 0,
"padding_y": 0,
"fontsize": 12,
"language": "en",
"colours": "bwr"
}
"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"
}
},
]

View File

@ -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"
}
},
]

View File

@ -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()

0
logs/dummy.txt.txt Normal file
View File