Adapted Inkycal_image
By using a helper class, this module could be simplified greatly
This commit is contained in:
		| @@ -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 = { | ||||
|      | ||||
|   'rotation':{ | ||||
|     "label":"Specify the angle to rotate the image. Default is 0", | ||||
|     "options": [0, 90, 180, 270, 360, "auto"], | ||||
|     "default":0, | ||||
|     "autoflip":{ | ||||
|         "label":"Should the image be flipped automatically?", | ||||
|         "options": [True, False] | ||||
|         }, | ||||
|  | ||||
|   '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) | ||||
|     logger.info(f'Image size: {im_size}') | ||||
|  | ||||
|     # initialize custom image class | ||||
|     im = Images() | ||||
|  | ||||
|     # use the image at the first index | ||||
|     im.load(self.path) | ||||
|  | ||||
|     # 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: | ||||
|         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.') | ||||
|       im_black, im_colour = im.to_colour() | ||||
|  | ||||
|     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(self.colours) | ||||
|  | ||||
|     # paste the images on the canvas | ||||
|     im_black.paste(black, (self.x, self.y)) | ||||
|     im_colour.paste(colour, (self.x, self.y)) | ||||
|  | ||||
|     # 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') | ||||
|     # 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') | ||||
|   | ||||
		Reference in New Issue
	
	Block a user