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:
		| @@ -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') | ||||
|   | ||||
		Reference in New Issue
	
	Block a user