Finished draft of inkycal_image module
In testing, might contain bugs! Split server settings from inkycal image. Inkycal_server will be done soon
This commit is contained in:
parent
cb162a5b1e
commit
29381d733c
@ -1,179 +1,305 @@
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Image module for inkycal Project
|
||||
Image module for Inkycal Project
|
||||
Copyright by aceisace
|
||||
Development satge: Beta
|
||||
"""
|
||||
|
||||
from os import path
|
||||
from inkycal.modules.template import inkycal_module
|
||||
from inkycal.custom import *
|
||||
|
||||
from PIL import ImageOps
|
||||
import requests
|
||||
import numpy
|
||||
|
||||
"""----------------------------------------------------------------"""
|
||||
#path = 'https://github.com/aceisace/Inky-Calendar/raw/master/Gallery/Inky-Calendar-logo.png'
|
||||
#path ='/home/pi/Inky-Calendar/images/canvas.png'
|
||||
path = inkycal_image_path
|
||||
path_body = inkycal_image_path_body
|
||||
mode = 'auto' # 'horizontal' # 'vertical' # 'auto'
|
||||
upside_down = False # Flip image by 180 deg (upside-down)
|
||||
alignment = 'center' # top_center, top_left, center_left, bottom_right etc.
|
||||
colours = 'bwr' # bwr # bwy # bw
|
||||
render = True # show image on E-Paper?
|
||||
"""----------------------------------------------------------------"""
|
||||
filename = os.path.basename(__file__).split('.py')[0]
|
||||
logger = logging.getLogger(filename)
|
||||
logger.setLevel(level=logging.DEBUG)
|
||||
|
||||
# First determine dimensions
|
||||
if mode == 'horizontal':
|
||||
display_width, display_height == display_height, display_width
|
||||
class Inkyimage(inkycal_module):
|
||||
"""Image class
|
||||
display an image from a given path or URL
|
||||
"""
|
||||
_allowed_layout = ['fill', 'center', 'fit', 'auto']
|
||||
_allowed_rotation = [0, 90, 180, 270, 360, 'auto']
|
||||
_allowed_colours = ['bw', 'bwr', 'bwy']
|
||||
|
||||
if mode == 'vertical':
|
||||
raise NotImplementedError('Vertical mode is not currenctly supported')
|
||||
def __init__(self, section_size, section_config):
|
||||
"""Initialize inkycal_rss module"""
|
||||
|
||||
# .. Then substitute possibly parameterized path
|
||||
# TODO Get (assigned) panel dimensions instead of display dimensions
|
||||
path = path.replace('{model}', model).replace('{width}',str(display_width)).replace('{height}',str(display_height))
|
||||
print(path)
|
||||
super().__init__(section_size, section_config)
|
||||
|
||||
"""Try to open the image if it exists and is an image file"""
|
||||
try:
|
||||
if 'http' in path:
|
||||
if path_body is None:
|
||||
# Plain GET
|
||||
im = Image.open(requests.get(path, stream=True).raw)
|
||||
# Module specific parameters
|
||||
required = ['path']
|
||||
for param in required:
|
||||
if not param in section_config:
|
||||
raise Exception('config is missing {}'.format(param))
|
||||
|
||||
# module name
|
||||
self.name = self.__class__.__name__
|
||||
|
||||
# module specific parameters
|
||||
self.image_path = self.config['path']
|
||||
|
||||
self.rotation = 0 #0 #90 # 180 # 270 # auto
|
||||
self.layout = 'fill' # centre # fit # auto
|
||||
self.colours = 'bw' #grab from settings file?
|
||||
|
||||
# give an OK message
|
||||
print('{0} loaded'.format(self.name))
|
||||
|
||||
def _validate(self):
|
||||
"""Validate module-specific parameters"""
|
||||
|
||||
# Validate image_path
|
||||
if not isinstance(self.image_path, str):
|
||||
print(
|
||||
'image_path has to be a string: "URL1" or "/home/pi/Desktop/im.png"')
|
||||
|
||||
# Validate layout
|
||||
if not isinstance(self.layout, str) or (
|
||||
self.layout not in self._allowed_layout):
|
||||
print('layout has to be one of the following:', self._allowed_layout)
|
||||
|
||||
# Validate rotation angle
|
||||
if self.rotation not in self._allowed_rotation:
|
||||
print('rotation has to be one of the following:', self._allowed_rotation)
|
||||
|
||||
# Validate colours
|
||||
if not isinstance(self.colours, str) or (
|
||||
self.colours not in self._allowed_colours):
|
||||
print('colour has to be one of the following:', self._allowed_colours)
|
||||
|
||||
def generate_image(self):
|
||||
"""Generate image for this module"""
|
||||
|
||||
# Define new image size with respect to padding
|
||||
im_width = self.width
|
||||
im_height = self.height
|
||||
im_size = im_width, im_height
|
||||
logger.info('image size: {} x {} px'.format(im_width, im_height))
|
||||
|
||||
# Try to open the image if it exists and is an image file
|
||||
try:
|
||||
if self.image_path.startswith('http'):
|
||||
logger.debug('identified url')
|
||||
self.image = Image.open(requests.get(self.image_path, stream=True).raw)
|
||||
else:
|
||||
logger.info('identified local path')
|
||||
self.image = Image.open(self.image_path)
|
||||
except FileNotFoundError:
|
||||
raise ('Your file could not be found. Please check the filepath')
|
||||
except OSError:
|
||||
raise ('Please check if the path points to an image file.')
|
||||
|
||||
logger.debug(('image-width:', self.image.width))
|
||||
logger.debug(('image-height:', self.image.height))
|
||||
|
||||
# Create an image for black pixels and one for coloured pixels
|
||||
im_black = Image.new('RGB', size = im_size, color = 'white')
|
||||
im_colour = Image.new('RGB', size = im_size, color = 'white')
|
||||
|
||||
# do the required operations
|
||||
self._remove_alpha()
|
||||
self._to_layout()
|
||||
black, colour = self._map_colours()
|
||||
|
||||
# paste the imaeges on the canvas
|
||||
im_black.paste(black, (self.x, self.y))
|
||||
if colour != None:
|
||||
im_colour.paste(colour, (self.x, self.y))
|
||||
|
||||
# Save image of black and colour channel in image-folder
|
||||
im_black.save(images+self.name+'.png', 'PNG')
|
||||
if colour != None:
|
||||
im_colour.save(images+self.name+'_colour.png', 'PNG')
|
||||
|
||||
def _rotate(self, angle=None):
|
||||
"""Rotate the image to a given angle
|
||||
angle must be one of :[0, 90, 180, 270, 360, 'auto']
|
||||
"""
|
||||
im = self.image
|
||||
if angle == None:
|
||||
angle = self.rotation
|
||||
|
||||
# Check if angle is supported
|
||||
if angle not in self._allowed_rotation:
|
||||
print('invalid angle provided, setting to fallback: 0 deg')
|
||||
angle = 0
|
||||
|
||||
# Autoflip the image if angle == 'auto'
|
||||
if angle == 'auto':
|
||||
if (im.width > self.height) and (im.width < self.height):
|
||||
print('display vertical, image horizontal -> flipping image')
|
||||
image = im.rotate(90, expand=True)
|
||||
if (im.width < self.height) and (im.width > self.height):
|
||||
print('display horizontal, image vertical -> flipping image')
|
||||
image = im.rotate(90, expand=True)
|
||||
# if not auto, flip to specified angle
|
||||
else:
|
||||
# POST request, passing path_body in the body
|
||||
im = Image.open(requests.post(path, json=path_body, stream=True).raw)
|
||||
else:
|
||||
im = Image.open(path)
|
||||
except FileNotFoundError:
|
||||
print('Your file could not be found. Please check the path to your file.')
|
||||
raise
|
||||
except OSError:
|
||||
print('Please check if the path points to an image file.')
|
||||
raise
|
||||
image = im.rotate(angle, expand = True)
|
||||
self.image = image
|
||||
|
||||
"""Turn image upside-down if specified"""
|
||||
if upside_down == True:
|
||||
im.rotate(180, expand = True)
|
||||
def _fit_width(self, width=None):
|
||||
"""Resize an image to desired width"""
|
||||
im = self.image
|
||||
if width == None: width = self.width
|
||||
|
||||
if mode == 'auto':
|
||||
if (im.width > im.height) and (display_width < display_height):
|
||||
print('display vertical, image horizontal -> flipping image')
|
||||
im = im.rotate(90, expand=True)
|
||||
if (im.width < im.height) and (display_width > display_height):
|
||||
print('display horizontal, image vertical -> flipping image')
|
||||
im = im.rotate(90, expand=True)
|
||||
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_width(image, width):
|
||||
"""Resize an image to desired width"""
|
||||
print('resizing width from', image.width, 'to', end = ' ')
|
||||
wpercent = (display_width/float(image.width))
|
||||
hsize = int((float(image.height)*float(wpercent)))
|
||||
img = image.resize((width, hsize), Image.ANTIALIAS)
|
||||
print(img.width)
|
||||
return img
|
||||
def _fit_height(self, height=None):
|
||||
"""Resize an image to desired height"""
|
||||
im = self.image
|
||||
if height == None: height = self.height
|
||||
|
||||
def fit_height(image, height):
|
||||
"""Resize an image to desired height"""
|
||||
print('resizing height from', image.height, 'to', end = ' ')
|
||||
hpercent = (height / float(image.height))
|
||||
wsize = int(float(image.width) * float(hpercent))
|
||||
img = image.resize((wsize, height), Image.ANTIALIAS)
|
||||
print(img.height)
|
||||
return img
|
||||
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
|
||||
|
||||
if im.width > display_width:
|
||||
im = fit_width(im, display_width)
|
||||
if im.height > display_height:
|
||||
im = fit_height(im, display_height)
|
||||
def _to_layout(self, mode=None):
|
||||
"""Adjust the image to suit the layout
|
||||
mode can be center, fit or fill"""
|
||||
|
||||
if alignment == 'center':
|
||||
x,y = int((display_width-im.width)/2), int((display_height-im.height)/2)
|
||||
elif alignment == 'center_right':
|
||||
x, y = display_width-im.width, int((display_height-im.height)/2)
|
||||
elif alignment == 'center_left':
|
||||
x, y = 0, int((display_height-im.height)/2)
|
||||
im = self.image
|
||||
if mode == None: mode = self.layout
|
||||
|
||||
elif alignment == 'top_center':
|
||||
x, y = int((display_width-im.width)/2), 0
|
||||
elif alignment == 'top_right':
|
||||
x, y = display_width-im.width, 0
|
||||
elif alignment == 'top_left':
|
||||
x, y = 0, 0
|
||||
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'
|
||||
|
||||
elif alignment == 'bottom_center':
|
||||
x, y = int((display_width-im.width)/2), display_height-im.height
|
||||
elif alignment == 'bottom_right':
|
||||
x, y = display_width-im.width, display_height-im.height
|
||||
elif alignment == 'bottom_left':
|
||||
x, y = display_width-im.width, display_height-im.height
|
||||
# If mode is center, just center the image
|
||||
if mode == 'center':
|
||||
pass
|
||||
|
||||
if len(im.getbands()) == 4:
|
||||
print('removing transparency')
|
||||
bg = Image.new('RGBA', (im.width, im.height), 'white')
|
||||
im = Image.alpha_composite(bg, im)
|
||||
# if mode is fit, adjust height of the image while keeping ascept-ratio
|
||||
if mode == 'fit':
|
||||
self._fit_height()
|
||||
|
||||
image.paste(im, (x,y))
|
||||
im = image
|
||||
# if mode is fill, enlargen or shrink the image to fit width
|
||||
if mode == 'fill':
|
||||
self._fit_width()
|
||||
|
||||
if colours == 'bw':
|
||||
"""For black-white images, use monochrome dithering"""
|
||||
black = im.convert('1', dither=True)
|
||||
elif colours == 'bwr':
|
||||
"""For black-white-red images, create corresponding palette"""
|
||||
pal = [255,255,255, 0,0,0, 255,0,0, 255,255,255]
|
||||
elif colours == 'bwy':
|
||||
"""For black-white-yellow images, create corresponding palette"""
|
||||
pal = [255,255,255, 0,0,0, 255,255,0, 255,255,255]
|
||||
# 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)
|
||||
|
||||
"""Map each pixel of the opened image to the Palette"""
|
||||
if colours != 'bw':
|
||||
palette_im = Image.new('P', (3,1))
|
||||
palette_im.putpalette(pal * 64)
|
||||
quantized_im = im.quantize(palette=palette_im)
|
||||
quantized_im.convert('RGB')
|
||||
# fit both height and width
|
||||
self._fit_height()
|
||||
self._fit_width()
|
||||
|
||||
"""Create buffer for coloured pixels"""
|
||||
buffer1 = numpy.array(quantized_im.convert('RGB'))
|
||||
r1,g1,b1 = buffer1[:, :, 0], buffer1[:, :, 1], buffer1[:, :, 2]
|
||||
if self.image.width > self.width:
|
||||
x = int( (self.image.width - self.width) / 2)
|
||||
else:
|
||||
x = int( (self.width - self.image.width) / 2)
|
||||
|
||||
"""Create buffer for black pixels"""
|
||||
buffer2 = numpy.array(quantized_im.convert('RGB'))
|
||||
r2,g2,b2 = buffer2[:, :, 0], buffer2[:, :, 1], buffer2[:, :, 2]
|
||||
if self.image.height > self.height:
|
||||
y = int( (self.image.height - self.height) / 2)
|
||||
else:
|
||||
y = int( (self.height - self.image.height) / 2)
|
||||
|
||||
if colours == 'bwr':
|
||||
"""Create image for only red pixels"""
|
||||
buffer2[numpy.logical_and(r2 == 0, b2 == 0)] = [255,255,255] # black->white
|
||||
buffer2[numpy.logical_and(r2 == 255, b2 == 0)] = [0,0,0] #red->black
|
||||
colour = Image.fromarray(buffer2)
|
||||
"""Create image for only black pixels"""
|
||||
buffer1[numpy.logical_and(r1 == 255, b1 == 0)] = [255,255,255]
|
||||
black = Image.fromarray(buffer1)
|
||||
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 == None: colours = self.colours
|
||||
|
||||
if colours not in self._allowed_colours:
|
||||
print('invalid colour: "{}", has to be one of: {}'.format(
|
||||
colours, self._allowed_colours))
|
||||
print('setting to fallback: bw')
|
||||
colours = 'bw'
|
||||
|
||||
if colours == 'bw':
|
||||
|
||||
# For black-white images, use monochrome dithering
|
||||
im_black = im.convert('1', dither=True)
|
||||
im_colour = None
|
||||
|
||||
elif colours == 'bwr':
|
||||
# For black-white-red images, create corresponding palette
|
||||
pal = [255,255,255, 0,0,0, 255,0,0, 255,255,255]
|
||||
|
||||
elif colours == 'bwy':
|
||||
# For black-white-yellow images, create corresponding palette"""
|
||||
pal = [255,255,255, 0,0,0, 255,255,0, 255,255,255]
|
||||
|
||||
# Map each pixel of the opened image to the Palette
|
||||
if colours == 'bwr' or colours == 'bwy':
|
||||
palette_im = Image.new('P', (3,1))
|
||||
palette_im.putpalette(pal * 64)
|
||||
quantized_im = im.quantize(palette=palette_im)
|
||||
quantized_im.convert('RGB')
|
||||
|
||||
# Create buffer for coloured pixels
|
||||
buffer1 = numpy.array(quantized_im.convert('RGB'))
|
||||
r1,g1,b1 = buffer1[:, :, 0], buffer1[:, :, 1], buffer1[:, :, 2]
|
||||
|
||||
# Create buffer for black pixels
|
||||
buffer2 = numpy.array(quantized_im.convert('RGB'))
|
||||
r2,g2,b2 = buffer2[:, :, 0], buffer2[:, :, 1], buffer2[:, :, 2]
|
||||
|
||||
if colours == 'bwr':
|
||||
# Create image for only red pixels
|
||||
buffer2[numpy.logical_and(r2 == 0, b2 == 0)] = [255,255,255] # black->white
|
||||
buffer2[numpy.logical_and(r2 == 255, b2 == 0)] = [0,0,0] #red->black
|
||||
im_colour = Image.fromarray(buffer2)
|
||||
|
||||
# Create image for only black pixels
|
||||
buffer1[numpy.logical_and(r1 == 255, b1 == 0)] = [255,255,255]
|
||||
im_black = Image.fromarray(buffer1)
|
||||
|
||||
if colours == 'bwy':
|
||||
# Create image for only yellow pixels
|
||||
buffer2[numpy.logical_and(r2 == 0, b2 == 0)] = [255,255,255] # black->white
|
||||
buffer2[numpy.logical_and(g2 == 255, b2 == 0)] = [0,0,0] #yellow -> black
|
||||
im_colour = Image.fromarray(buffer2)
|
||||
|
||||
# Create image for only black pixels
|
||||
buffer1[numpy.logical_and(g1 == 255, b1 == 0)] = [255,255,255]
|
||||
im_black = Image.fromarray(buffer1)
|
||||
|
||||
return im_black, im_colour
|
||||
|
||||
@staticmethod
|
||||
def save(image):
|
||||
im = self.image
|
||||
im.save('/home/pi/Desktop/test.png', 'PNG')
|
||||
|
||||
@staticmethod
|
||||
def _show(image):
|
||||
"""Preview the image on gpicview (only works on Rapsbian with Desktop)"""
|
||||
path = '/home/pi/Desktop/'
|
||||
image.save(path+'temp.png')
|
||||
os.system("gpicview "+path+'temp.png')
|
||||
os.system('rm '+path+'temp.png')
|
||||
|
||||
if __name__ == '__main__':
|
||||
print('running {0} in standalone/debug mode'.format(filename))
|
||||
|
||||
## a = Inkyimage((480,800), {'path': "https://raw.githubusercontent.com/aceisace/Inky-Calendar/dev_ver2_0/Gallery/logo.png"})
|
||||
## a.generate_image()
|
||||
|
||||
if colours == 'bwy':
|
||||
"""Create image for only yellow pixels"""
|
||||
buffer2[numpy.logical_and(r2 == 0, b2 == 0)] = [255,255,255] # black->white
|
||||
buffer2[numpy.logical_and(g2 == 255, b2 == 0)] = [0,0,0] #yellow -> black
|
||||
colour = Image.fromarray(buffer2)
|
||||
"""Create image for only black pixels"""
|
||||
buffer1[numpy.logical_and(g1 == 255, b1 == 0)] = [255,255,255]
|
||||
black = Image.fromarray(buffer1)
|
||||
##
|
||||
##if render == True:
|
||||
## epaper = driver.EPD()
|
||||
## print('Initialising E-Paper...', end = '')
|
||||
## epaper.init()
|
||||
## print('Done')
|
||||
##
|
||||
## print('Sending image data and refreshing display...', end='')
|
||||
## if three_colour_support == True:
|
||||
## epaper.display(epaper.getbuffer(black), epaper.getbuffer(colour))
|
||||
## else:
|
||||
## epaper.display(epaper.getbuffer(black))
|
||||
## print('Done')
|
||||
##
|
||||
## print('Sending E-Paper to deep sleep...', end = '')
|
||||
## epaper.sleep()
|
||||
print('Done')
|
||||
|
Loading…
Reference in New Issue
Block a user