Merge pull request #145 from aceisace/feature/images_refactoring
Feature/images refactoring
This commit is contained in:
commit
2f2011583c
5
.gitignore
vendored
5
.gitignore
vendored
@ -10,3 +10,8 @@ lib/
|
||||
share/
|
||||
lib64
|
||||
pyvenv.cfg
|
||||
*.m4
|
||||
*.h
|
||||
*.guess
|
||||
*.log
|
||||
*.in
|
||||
|
@ -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
|
||||
|
@ -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():
|
||||
|
@ -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()
|
||||
|
@ -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')
|
||||
|
@ -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
|
@ -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')
|
||||
|
246
inkycal/modules/inky_image.py
Normal file
246
inkycal/modules/inky_image.py
Normal 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')
|
@ -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')
|
||||
|
@ -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')
|
||||
|
@ -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')
|
||||
|
@ -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')
|
||||
|
@ -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')
|
||||
|
135
inkycal/modules/inkycal_slideshow.py
Normal file
135
inkycal/modules/inkycal_slideshow.py
Normal 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')
|
@ -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')
|
||||
|
@ -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')
|
||||
|
@ -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')
|
||||
|
@ -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__,
|
||||
|
@ -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"
|
||||
}
|
||||
},
|
||||
]
|
||||
|
@ -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"
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
|
@ -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"
|
||||
}
|
||||
},
|
||||
]
|
||||
|
108
inkycal/tests/inkycal_slideshow_test.py
Normal file
108
inkycal/tests/inkycal_slideshow_test.py
Normal 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
0
logs/dummy.txt.txt
Normal file
Loading…
Reference in New Issue
Block a user