diff --git a/inkycal/modules/inkycal_image.py b/inkycal/modules/inkycal_image.py index 43b8a57..b97e6f0 100644 --- a/inkycal/modules/inkycal_image.py +++ b/inkycal/modules/inkycal_image.py @@ -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')