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 = {
 | 
			
		||||
    
 | 
			
		||||
    "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')
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user