1
\$\begingroup\$

I have added a few new features to Pygame Slideshow that includes using curses that works with ssh, a way to move pictures to a different directory while viewing them, a pause, and redisplay mode.

A code review with any improvements or commits would be nice.

#!/usr/bin/env python

"""
  Picture Slideshow2 that uses pygame, curses, and works with SSH.

  A slideshow program that also helps to organize pictures in
  folders. This program will loop continuous on the directory
  (from user configuration or command line argument) and sub-
  directories skipping thoses in the configurable ignore list.
  It will display pictures in regular size or reduce them if they
  are larger than the configurable screen size. It will only
  display files with extension in the configuration file list of
  my_image_file. To exit the program, click the mouse in the window
  or exit button. Curses controls the local or SSH keyboard in
  which the q, ESC, or CTRL-C key will also exit the program. The
  z or down arrow key will freeze the display, any key will resume
  the program. The left arrow key will redisplay previous pictures
  in sleep mode with the right arrow key moving in the forward
  direction. The up arrow key will resume the program. A configurable
  mv_dir_dict allow moving the displayed picture file to a different
  directory (folder) using the keyboard.
  Thanks to Gareth Rees for his valuable input.
  I have only tested on the Raspberry Pi 3 & 0W, raspbian Jessie.
"""


import os
import sys
import pygame
from pygame.locals import FULLSCREEN
import curses
from curses import wrapper
from colorama import Fore, Back, Style

#  User Configurations

Title = 'Picture  Slideshow2'
display_time = 3.5   # Time in seconds.
full_screen = False  # False equal window.
# Custom window size setting, less than 100, 50 = full size.
win_size = 0, 0
# win_size = 640, 480
# Adjust negative height of window for menu and title bar.
adj_height = 76
my_image_files = '.bmp', '.jpg', '.JPG', '.png', '.gif', '.tif'
Dir = 'Pictures'
# Directories to ignore.
ignore = 'Junk', 'Trash', 'Sort'
# Dictionary to move display picture to different directory.
mv_dir_dict = {
    'f': 'Flowers', 'j': 'Junk', 's': 'Sort', 'l': 'Landscape',
    'b': 'Birds', 'c': 'Cars', 't': 'Trash', 'p': 'People',
    'w': 'Wild_Life', 'o': 'Our_Family', 'v': 'Vacation',
    'n': 'Not_Good'
    }  # Limit to 20 or less.

#  Setup Variables

os.environ['DISPLAY'] = ':0.0'  # Local attached display.
os.environ['ESCDELAY'] = '2'    # Prevents delay on escape code.
time_out = 3000  # 3 seconds time out prevents start up hang.
light_grey = 211, 211, 211
grey = 128, 128, 128
black = 0, 0, 0
blue = 0, 0, 144
magenta = 100, 0, 100
CR = '\r'
MV = '/bin/mv '
ESC = '\x1b'
Normal = Style.RESET_ALL + CR
not_found = (Fore.RED + '{} ' + Fore.WHITE + Back.RED + '{}' +
            Normal[:-1] + Fore.RED + ' NOT FOUND!' + Normal)
err_kb = (Back.RED + '{}' + Normal)
previous_dir = None
list_filen = []
indx = -1


def terminal_print(filename):
    """Prints filename on terminal with the time the picture was
    displayed on the screen. Also prints the directory name when
    the directory changes.
    """
    global previous_dir
    dir_path = os.path.dirname(filename)
    if previous_dir != dir_path:
        print(Fore.MAGENTA + dir_path + Normal)
        previous_dir = dir_path
    seconds_delayed, ms = divmod(loopclk.tick(), 1000)
    print('{0:60} {1:4d}.{2:03d}{3:1}'.format(\
            filename, seconds_delayed, ms, CR))


def redisplay(back, err=False):
    """This function is called from the left and right arrow keys
    in redisplay mode to move backward or forward in displaying
    the pictures file and printing. It returns error True if there
    is an out of limits condition.
    """
    global indx
    if back:
        if indx + 1 < len(list_filen):
            indx += 1
        else:
            err = True
    else:
        if indx > 0:
            indx -= 1
        else:
            err = True
    if not err:
        background(list_filen[indx], True)
        sys.stdout.write(Fore.GREEN + list_filen[indx] + '   ')
    return err


def kb_arrow(kb_key, err=False):
    """The left arrow key stops the progression of displaying
    pictures and redisplay the previous picture. There are eight
    steps back, one current, and one forward. The right arrow key
    will redisplay pictures in the forward direction. The up arrow
    key will continue the original sequence. An error will display
    on keys out of limits (except q and ESC, which terminate the
    program).
    """
    global indx
    while True:
        if kb_key == 'KEY_LEFT':
            indx = abs(indx)
            err = redisplay(back=True)
        elif kb_key == 'KEY_RIGHT':
            err = redisplay(back=False)
        elif kb_key == 'KEY_UP':
            background(list_filen[1], True)
            terminal_print(list_filen[1])
            background(list_filen[0])
            indx = -1
            return
        if err:
            sys.stdout.write(Back.RED + str(kb_key) + \
                    Normal[:-1] + '   ')
        kb_key = kb_sleep()


def kb_sleep():
    """Sleep until another key is depressed. Checks events while
    sleeping by calling read_kb. Return kb_key to caller.
    """
    print(Fore.CYAN + 'zzz' + Normal)
    kb_key = ''
    while not kb_key:
        pygame.time.wait(150)
        kb_key = read_kb()
    return kb_key


def move_file(key):
    """Move displayed file to key selected directory in mv_dir_dict.
    The displayed file is saved in list_filen[1], unless in previous
    display mode, in which it would be list_filen[indx]. If not in
    previous display mode, indx = -1.
    """
    fnum = abs(indx)
    new_dir = os.path.join(Dir, mv_dir_dict[key], '')
    if os.path.isdir(new_dir):
        new_filen = new_dir + os.path.basename(list_filen[fnum])
        old_new_filen = list_filen[fnum] + ' {} ' + new_filen
        if list_filen[fnum] == new_filen:
            print(Back.RED + old_new_filen.format('==') + Normal)
        else:
            print(Fore.YELLOW + old_new_filen.format('=>') + Normal)
            os.system(MV + old_new_filen.format(''))
            pygame.display.set_caption(old_new_filen.format('=>'))
            list_filen[fnum] = new_filen
    else:
        print(not_found.format('Dir',new_dir))


def mouse_events():
    """Manage local mouse events. Mouse pointer is set to
    invisible in display screen.  Will exit program if mouse button
    is pressed in display window or on the exit button.
    """
    events = pygame.event.get()
    for event in events:
        if event.type == pygame.MOUSEBUTTONUP or \
                event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()


def read_kb():
    """First checks for mouse events, which can terminate the
    program. Next get the keyboard key if pressed. Then check for
    q or ESC key to terminate the program, a key in the mv_dir_dict
    to move the file to a different directory, or return the keyboard
    key, if any, to the caller.
    """
    mouse_events()
    terminal.nodelay(True)
    kb_key = ''
    try:
        kb_key = terminal.getkey()
    except curses.error:
        return ''
    else:
        if kb_key == 'q' or kb_key == ESC:
            pygame.quit()
            sys.exit()
        elif kb_key in mv_dir_dict and len(list_filen) > 1:
            move_file(kb_key)
            return ''
        else:
            return kb_key


def key_control():
    """The entry of managing the local or SSH keyboard. Call read_kb
    to get the keyboard key, except for the two events which are
    handled in the read_kb function. The three events handled here
    are sleep with the z or down arrow key, redisplay and hold the
    previous picture on left arrow key, and error print other keys.
    """
    kb_key = read_kb()
    if kb_key > '':
        if kb_key == 'z' or kb_key == 'KEY_DOWN':
            kb_sleep()
        elif kb_key == 'KEY_LEFT' and len(list_filen) > 2:
            kb_arrow(kb_key)
        else:
            print(err_kb.format(str(kb_key)))


def display_timer():
    """Time picture is displayed. Check events in wait_time by
    calling key_control.
    """
    key_control()
    wait_time = int(display_time * 1000) - mainclk.tick()
    while wait_time > 200:
        wait_time -= 200
        pygame.time.wait(200)
        key_control()
    if wait_time > 0:
        pygame.time.wait(wait_time)
        key_control()


def image_scale_blit(image):
    """Scale down image (picture) if larger that screen and blit
    image to center of screen, slightly to the top.
    """
    image_size = image_width, image_height = image.get_size()
    if image_size <= win_size and image_size[1] <= win_size[1]:
        dsp_width, dsp_height = image_size
    else:
        scale_factor = height / float(image_height)
        dsp_width = int(scale_factor * image_width)
        dsp_height = int(scale_factor * image_height)
        if dsp_width > width:
            scale_factor = width / float(image_width)
            dsp_width = int(scale_factor * image_width)
            dsp_height = int(scale_factor * image_height)
        image = pygame.transform.scale(image, (dsp_width, dsp_height))
    xleft = int((width - dsp_width) / 2)
    ytop = int((height - dsp_height) / 3)
    rect = xleft, ytop, xleft + dsp_width, ytop + dsp_height
    screen.blit(image, rect)


def background(image_file, fast=False):
    """Load picture and check for events as loading takes a bit of
    time. Call image_scale_blit to scale and blit the image. The
    display_timer holds the picture on the screen for the configurable
    time before the screen is flipped and caption is set to filename.
    """
    image = pygame.image.load(image_file)
    key_control()
    screen.fill(grey)
    image_scale_blit(image)
    if not fast:
        display_timer()
    pygame.display.flip()
    mainclk.tick()
    fname = os.path.basename(image_file)
    pygame.display.set_caption(image_file, fname)


def iterate_files(Dir, ignored=()):
    """Generate files in and below the top directory in sorted
    order, ignoring directories with names in the ignored iterable.
    """
    ignored = set(ignored)
    for root, dirs, files in os.walk(Dir):
        for file in sorted(files):
            yield os.path.join(root, file)
        dirs[:] = sorted(set(dirs) - ignored)


def pic_files(Dir):
    """Call background and terminal_print for each file in and below
    the top directory from the iterate_files function that has a file
    extension in my_image_file. Save a list of ten image filenames
    that can be redisplayed. Time-out if no image files are found in
    the selected directory (Dir).
    """
    for file in iterate_files(Dir, ignored=ignore):
        _, extension = os.path.splitext(file)
        if extension in my_image_files:
            list_filen.insert(0, file)
            if len(list_filen) == 12:
                list_filen.remove(list_filen[11])
            background(file)
            terminal_print(file)
        elif not list_filen and pygame.time.get_ticks() > time_out:
            curses.endwin()
            pygame.time.wait(500)
            print(Fore.RED + 'Files with extention of' + Normal)
            print(err_kb.format(my_image_files))
            print(not_found.format('in Dir of', Dir))
            pygame.quit()
            sys.exit(1)


def main(terminal_screen):
    """Use curses wrapper to switch and manage terminal. Prints
    title, screen size, and start loop to display pictures.
    """
    global terminal
    terminal = terminal_screen
    terminal.clear()
    terminal.refresh()
    terminal.keypad(True)
    curses.curs_set(0)
    print(Fore.CYAN + Title + '      ' + str(scr_size) + Normal)
    while True:
        print(CR)
        pic_files(Dir)


def menu_2(font_size, x_pos, y_pos):
    """Position and blit right side menu (the key dict to move files
    to a different directory).
    """
    x = x_pos[3]
    y = y_pos[2] + y_pos[0]
    cnt = 0
    myfont = pygame.font.Font(None, int(font_size[2]))
    for dict_key in sorted(mv_dir_dict):
        if cnt == 10:
            x = x * 2 - x_pos[1]
            y = y_pos[2] + y_pos[0]
        line = dict_key + '  ' + mv_dir_dict[dict_key]
        self = myfont.render(line, 1, (magenta))
        screen.blit(self, (x + x_pos[1], y))
        y += y_pos[0]
        cnt += 1


def menu_1(font_size, x_pos, y_pos):
    """Position and blit left side menu (for quit, sleep, and to
    redisplay pictures).
    """
    y = y_pos[2] + y_pos[0]
    myfont = pygame.font.Font(None, int(font_size[2]))
    mesg = 'q or ESC  Quit', 'z or D_Arrow  Sleep',\
            '   anykey Awake', 'L_Arrow  Back',\
            '   R_Arrow  Forward', '   U_Arrow  Continue'
    for line in mesg:
        self = myfont.render(line, 1, (blue))
        screen.blit(self, (x_pos[2], y))
        y += y_pos[0]


def sub_title(font_size, x_pos, y_pos):
    """Position and blit medium size font for sub-titles to menus.
    """
    myfont = pygame.font.Font(None, font_size[1])
    self = myfont.render('Key Control', 1, (black))
    screen.blit(self, (x_pos[0], y_pos[1]))
    self = myfont.render('Key  Move to Directory', 1, (black))
    screen.blit(self, (x_pos[3], y_pos[1]))


def title(font_size, y_pos):
    """Position and blit large font size for title.
    """
    pygame.display.set_caption('Title')
    myfont = pygame.font.Font(None, font_size[0])
    self = myfont.render(Title, 1, (black))
    text_pos = self.get_rect()
    text_pos.centerx = screen.get_rect().centerx
    text_pos.y = y_pos[0]
    screen.blit(self, text_pos)


def heading():
    """Set up and display heading and keyboard menu while getting
    the first picture ready for display. The font_size, x_pos and
    y_pos are pre-scaled to the percentage of the screen width and
    height. Call the functions for titles and menus. Display flip.
    """
    font_size = [int(width / 19 + height / 21), int(width / 30\
            + height / 33), int(width / 39 + height / 43)]
    x_pos = [int(width / 64), int(width / 32), int(width / 21),\
            int(width/3)]
    y_pos = [int(height / 18), int(height / 5), int(height / 4)]
    screen.fill(light_grey)
    pygame.mouse.set_visible(0)
    title(font_size, y_pos)
    sub_title(font_size, x_pos, y_pos)
    menu_1(font_size, x_pos, y_pos)
    menu_2(font_size, x_pos, y_pos)
    pygame.display.flip()


#  Start program

"""Check validity of selected directory, initlize PyGame, set screen
size, display heading, start clocks, and start Curses (keyboard
control) which begins displaying pictures.
"""

if len(sys.argv) == 2:
    _, Dir = sys.argv
if not os.path.isdir(Dir):
    print(not_found.format('Dir', Dir))
    sys.exit()

pygame.init()
info = pygame.display.Info()
scr_size = info.current_w, info.current_h
if full_screen:
    screen = pygame.display.set_mode(scr_size, FULLSCREEN)
else:
    if win_size < (100, 50) or win_size[1] < 50:
        win_size = info.current_w, info.current_h - adj_height
    screen = pygame.display.set_mode(win_size)
win_size = width, height = pygame.display.get_surface().get_size()

heading()
mainclk = pygame.time.Clock()
loopclk = pygame.time.Clock()

wrapper(main)
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.