4
\$\begingroup\$

I have written a basic slideshow program that uses pygame to manage the display screen. It cycles through all pictures files in the directory and sub-directories. There is a user configuration section for the start directory, sub-directories to ignore, image file type, display time, full screen or window display and adj_height. I like the window display to see the title of the picture. On the Raspberry Pi, I changed the title font to size 18, color blue in Menu, Preferences, Appearance Setting. The window display has a height adjustment to compensate for menu and title bar as I could not find how to get maximum window height before making the window.

I would like a code review. My start program seems a little awkward. Any other points would be great.

#!/usr/bin/env python

"""
  Picture Slideshow that uses pygame.

  This slideshow program will loop continuous on the directory,
  from user configuration or command line argument, and sub-
  directories skipping thoses in the ignore list. It will display
  pictures in regular size or reduce them if bigger than the
  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. The keyboard ESC
  or q key will also exit the program, on ssh terminal use CTRL-C.
  Only tested on the Raspberry Pi 3.
"""

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

#  User Configurations

display_time = 2.5   # Time in seconds
full_screen = False  # False equal window
# full_screen = True
Dir = '/home/pi/Pictures'
# Directories to ignore
ignore = 'Junk', 'Trash', 'Paintings'
my_image_files = '.bmp', '.jpg', '.JPG', '.png', '.gif', '.tif'
Title = 'Picture  Slideshow'
# Adjust height of window for menu and title bar.
adj_height = 76

#  Setup Variables

os.environ['DISPLAY'] = ':0.0'
# grey = 211, 211, 211
grey = 128, 128, 128
black = 0, 0, 0
magenta = 100, 0, 100
Normal = Style.RESET_ALL
current_filename = 'None'


class Background(pygame.sprite.Sprite):
    # Use pygame to display pictures as background, scale to fit.

    def __init__(self, image_file):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.image.load(image_file)
        kb_mouse_events()
        # It takes time loading file, check for events.
        image_size = image_width, image_height = self.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)
        dsp_size = dsp_width, dsp_height
        self.image = pygame.transform.scale(self.image, dsp_size)
        xleft, ytop, xright, ybottom = self.image.get_rect()
        xleft = int(width - dsp_width)/2
        xright += xleft
        ytop = int(height - dsp_height)/3
        ybottom += ytop
        self.rect = xleft, ytop, xright, ybottom


def kb_mouse_events():
    # Manage keyboard and mouse events.

    events = pygame.event.get()
    keys = pygame.key.get_pressed()
    if keys[pygame.K_q] or keys[pygame.K_ESCAPE]:
        pygame.quit()
        sys.exit()
    for event in events:
        if event.type == pygame.MOUSEBUTTONUP or\
                event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()


def display_timer():
    # Time picture is displayed. Check keyboard in wait_time.

    kb_mouse_events()
    wait_time = int(display_time * 1000) - mainclk.tick()
    while wait_time > 200:
        wait_time -= 200
        pygame.time.wait(200)
        kb_mouse_events()
    pygame.time.wait(wait_time)
    kb_mouse_events()
    mainclk.tick()


def new_pic(filename):
    # Accept and display only pictures (my_image_files).

    _, extension = os.path.splitext(filename)
    if extension in my_image_files:
        global current_filename
        fname = os.path.basename(filename)
        dir_path = os.path.dirname(filename)
        previous_dir_path = os.path.dirname(current_filename)
        screen.fill(grey)
        back_ground = Background(filename)
        screen.blit(back_ground.image, back_ground.rect)
        display_timer()
        seconds_delayed, ms = divmod(loopclk.tick(), 1000)
        if previous_dir_path != dir_path:
            print(Fore.MAGENTA + dir_path + Normal + '\r')
        print('{0:60} {1:4d}.{2:03d}{3:1}'.format(\
                filename, seconds_delayed, ms, '\r'))
        pygame.display.flip()
        pygame.display.set_caption(filename, fname)
        current_filename = filename
        pygame.mouse.set_visible(0)
        # Mouse is poked to keep display from sleep.


def pic_files(Dir):
    # Sends files to new_pic, recursively.

    files = next(os.walk(Dir))[2]
    for file in sorted(files):
        filename = (Dir + '/' + file)
        new_pic(filename)
    dirs = next(os.walk(Dir))[1]
    sel_dirs = list(set(dirs) - set(ignore))
    for dir in sorted(sel_dirs):
        next_dir = (Dir + '/' + dir)
        pic_files(next_dir)


def heading():
    # Display heading while setting up pictures.

    screen.fill(grey)
    pygame.mouse.set_visible(0)
    pygame.display.set_caption('Title')
    myfont = pygame.font.Font(None, 96)
    self = myfont.render(Title, 1, (black))
    xleft, ytop, xright, ybottom = self.get_rect()
    xleft = int(width - xright)/2
    screen.blit(self, (xleft, 50))
    pygame.display.flip()
    pic_files(Dir)
    while True:
        print(Fore.MAGENTA + Dir + Normal + '\r')
        pic_files(Dir)


#  Start program

if len(sys.argv) == 2:
    _, Dir = sys.argv
pygame.init()
scr = pygame.display.Info()
win_size = width, height = scr.current_w, scr.current_h - adj_height
if full_screen:
    screen = pygame.display.set_mode(win_size, FULLSCREEN)
else:
    screen = pygame.display.set_mode(win_size)
win_size = width, height = pygame.display.get_surface().get_size()

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

I ran this program a couple days ago for six hours and had an abort, out of memory error. The abort was in the class section at the init statement. I changed the class Background(pygame.sprite.Sprite): to a function, def background(image_file): and have had the program running over twenty-four hours without any problems. I left the original post intack, adding the changes that I made below.

def background(image_file):
    """Display pictures as background, scale to fit."""
    image = pygame.image.load(image_file)
    key_control(terminal)
    # It takes time loading file, check for events.
    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)
    dsp_size = dsp_width, dsp_height
    image = pygame.transform.scale(image, dsp_size)
    xleft, ytop, xright, ybottom = image.get_rect()
    xleft = int(width - dsp_width)/2
    xright += xleft
    ytop = int(height - dsp_height)/3
    ybottom += ytop
    rect = xleft, ytop, xright, ybottom
    screen.blit(image, rect)

In the def new_pic(filename): function, I replaced back_ground = Background(filename) and screen.blit(back_ground.image, back_ground.rect) with background(filename).

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

1. Review

There's quite a lot of code here, so I'm going to look at just one part of it, namely the function pic_files.

  1. This function has two tasks: first, it has to find files that might contain images, and second, it has to call the function new_pic for each image. This makes it hard to test the code (how can you be sure that it found the right set of files?) and hard to reuse in other programs (because you'd probably have to change it to call some other function).

    It would be better to split these tasks into two functions, then you could reuse one of them. What you want is to have one function that generates the filenames, and another that calls new_pic. See the revised code below for how this can be done.

  2. The function calls os.walk twice, first to get the files:

    files = next(os.walk(Dir))[2]
    

    and second to get the directories:

    dirs = next(os.walk(Dir))[1]
    

    It would be better to call it just once and remember the result. You can use tuple unpacking to avoid the [2] and [1]:

    _, dirs, files = next(os.walk(dir))
    
  3. It would be a good idea if the list of ignored directories were an argument to the function. This would make the function easier to reuse, because you could pass different lists of ignored directories in different circumstances.

  4. This function only uses the first result generated by os.walk, and implements its own recursion over the directory tree. I guess that's because of the need to ignore some of the directories. But in fact os.walk already has built-in support for pruning the directory tree. The documentation says:

    When topdown is True [which it is by default — GDR], the caller can modify the dirnames list in-place (perhaps using del or slice assignment), and walk() will only recurse into the subdirectories whose names remain in dirnames; this can be used to prune the search [or] impose a specific order of visiting.

2. Revised code

def iterfiles(top, ignored=()):
    """Generate files 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(top):
        for f in sorted(files):
            yield os.path.join(root, f)
        dirs[:] = sorted(set(dirs) - ignored)

def pic_files(top):
    """Call new_pic for each file below the top directory."""
    for f in iterfiles(top, ignored=ignore):
        new_pic(f)
\$\endgroup\$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.