lang/python/ SimpleSlideshow


This assumes your files are .jpg files in a 1-deep tree, so example filenames are my_folder_1/my_image.jpg. Accessing the url http://the_server:4000/next returns a simple json document of the form { "path": "path_to/image.jpg" }. Accessing http://the_server:4000/ returns the html below, and accessing a url ending in .jpg attempts to serve the file. The image slides left-right or up-down depending on aspect ratios, as a kind of poor-man's Ken Burns effect. (At some point I may try to write a proper Ken Burns implementation, and crossfade between images.)

Python server

#!/usr/bin/env python3

# Python 3 server example
from http.server import BaseHTTPRequestHandler, HTTPServer
from subprocess import run, PIPE
import time
import sys
import re
import random
import os
import json
from glob import glob
from datetime import datetime
from threading import Thread

hostName = ""
serverPort = 4000

index_html = open("index.html").read()
files_glob = "*/*.jpg"
print(f"Indexing")
files = glob(files_glob)
def refresh_worker():
  global files
  files = glob(files_glob)

class MyServer(BaseHTTPRequestHandler):
  def homepage(self):
    self.send_response(200)
    self.send_header("Content-type", "text/html")
    self.end_headers()
    self.wfile.write(bytes(index_html, "utf-8"))
  def refresh(self):
    Thread(target=refresh_worker)
    self.send_response(200)
    self.send_header("Content-type", "text/html")
    self.end_headers()
    self.wfile.write(bytes("refresh requested", "utf-8"))
  def nextimg(self):
    src = random.choice(files)
    d = { "path": f"/{src}" }
    self.send_as_json(d)
  def sendimg(self,src):
    print(f"img {src}")
    if os.path.exists(src):
      self.send_response(200)
      self.send_header("Content-type", "image/jpeg")
      self.end_headers()
      with open(src,"rb") as f:
        self.wfile.write(f.read())
    else:
      self.send_response(404)
      self.send_header("Content-type", "text/html")
      self.end_headers()
      self.wfile.write(bytes(f"File {src} not found","utf8"))
  def send_as_json(self,d,response_code=200):
    j = json.dumps(d)
    self.send_json(j,response_code)
  def send_json(self,j,response_code=200):
    self.send_response(response_code)
    self.send_header("Content-type", "application/json")
    self.end_headers()
    self.wfile.write(bytes(j, "utf-8"))
  def do_GET(self):
    if self.path == "/favicon.ico":
      self.send_response(404)
      self.end_headers()
      self.wfile.write(b"No favicon")
    p = self.path.strip("/")
    if p == "":
      return self.homepage()
    if p == "next":
      return self.nextimg()
    if p == "refresh":
      return self.refresh()
    if p.endswith(".jpg"):
      return self.sendimg(p)
    if p == "quit":
      self.send_response(400)
      self.end_headers()
      self.wfile.write(b"Quitting")
      self.server.server_close()
    else:
      self.send_response(400)
      self.end_headers()
      self.wfile.write(b"Bad request")

if __name__ == "__main__":    
  print(f"Starting server")
  webServer = HTTPServer((hostName, serverPort), MyServer)
  print("Server started http://%s:%s" % (hostName, serverPort))

  try:
    webServer.serve_forever()
  except KeyboardInterrupt:
    pass

  webServer.server_close()
  print("Server stopped.")

Python Server for Zip Files

Launch this server.py in a folder containing .zip files of .jpg images, and it will serve those without needing to decompress beforehand.

#!/usr/bin/env python3

# Python 3 server example
from http.server import BaseHTTPRequestHandler, HTTPServer
from subprocess import run, PIPE
import time
import sys
import re
import random
import os
import json
from glob import glob
from datetime import datetime
from threading import Thread
import zipfile

hostName = ""
serverPort = 4000

zips_glob = "*.zip"
print(f"Indexing")
zips = []
files = []
def refresh_worker():
  global zips
  global files
  nzips = glob(zips_glob)
  nfiles = []
  for z in nzips:
    with zipfile.ZipFile(z) as f:
      for n in f.namelist():
        if n.endswith(".jpg"):
          nfiles.append((z,n))
  zips = nzips
  files = nfiles
refresh_worker()

class MyServer(BaseHTTPRequestHandler):
  def homepage(self):
    index_html = open("index.html").read()
    self.send_response(200)
    self.send_header("Content-type", "text/html")
    self.end_headers()
    self.wfile.write(bytes(index_html, "utf-8"))
  def refresh(self):
    Thread(target=refresh_worker)
    self.send_response(200)
    self.send_header("Content-type", "text/html")
    self.end_headers()
    self.wfile.write(bytes("refresh requested", "utf-8"))
  def nextimg(self):
    src = random.choice(files)
    z,n = src
    d = { "path": f"/{z}/{n}" }
    self.send_as_json(d)
  def sendimg(self,src):
    print(f"img {src}")
    z, n = src.split("/",1)
    print(f"{z=} {n=}")
    if os.path.exists(z):
      with zipfile.ZipFile(z) as f:
        print(f"Opened zip")
        try:
          with f.open(n,"r") as g:
            data = g.read()
            self.send_response(200)
            self.send_header("Content-type", "image/jpeg")
            self.end_headers()
            self.wfile.write(data)
            return
        except Exception as e:
          print(f"Exception {e}")
    self.send_response(404)
    self.send_header("Content-type", "text/html")
    self.end_headers()
    self.wfile.write(bytes(f"File {src} not found","utf8"))
  def send_as_json(self,d,response_code=200):
    j = json.dumps(d)
    self.send_json(j,response_code)
  def send_json(self,j,response_code=200):
    self.send_response(response_code)
    self.send_header("Content-type", "application/json")
    self.end_headers()
    self.wfile.write(bytes(j, "utf-8"))
  def do_GET(self):
    if self.path == "/favicon.ico":
      self.send_response(404)
      self.end_headers()
      self.wfile.write(b"No favicon")
    p = self.path.strip("/")
    if p == "":
      return self.homepage()
    if p == "next":
      return self.nextimg()
    if p == "refresh":
      return self.refresh()
    if p.endswith(".jpg"):
      return self.sendimg(p)
    if p == "quit":
      self.send_response(400)
      self.end_headers()
      self.wfile.write(b"Quitting")
      self.server.server_close()
    else:
      self.send_response(400)
      self.end_headers()
      self.wfile.write(b"Bad request")

if __name__ == "__main__":    
  print(f"Starting server")
  webServer = HTTPServer((hostName, serverPort), MyServer)
  print("Server started http://%s:%s" % (hostName, serverPort))

  try:
    webServer.serve_forever()
  except KeyboardInterrupt:
    pass

  webServer.server_close()
  print("Server stopped.")

HTML Javascript client

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Slideshow</title>
  <script>
    addEventListener("load",_ => {
      const q = (x,y=document) => y.querySelector(x)
      const qq = (x,y=document) => Array.from(y.querySelectorAll(x))
      const img = new Image()
      const fps = 60
      const secs = 1000
      const kbtime = 5 * secs
      const changetime = 60 * secs
      document.body.append(img)
      let iwidth, iheight
      let owidth, oheight
      let kbdirection, kbrange, kbinc
      let t0
      const change = _ => {
        fetch("/next").then(response => response.json()).then(data => {
          const newimg = new Image()
          newimg.src = data.path
          newimg.onload = _ => {
            img.src = newimg.src
            iwidth = newimg.width
            iheight = newimg.height
            compute()
          }
          newimg.src = data.path
        })
      }
      const compute = _ => {
        started = true
        const wwidth = window.innerWidth
        const wheight = window.innerHeight
        const iratio = iwidth / iheight
        const wratio = wwidth / wheight
        if( wratio >= iratio ) {
          // window is wider than image, so fit width
          owidth = wwidth
          oheight = iheight * wwidth / iwidth
          kbdirection = "vertical"
          kbrange = oheight - wheight
        } else {
          oheight = wheight
          owidth = iwidth * wheight / iheight
          kbdirection = "horizontal"
          kbrange = owidth - wwidth
        }
        img.width = owidth
        img.height = oheight
        kbinc = kbrange / kbtime // pixels per millisecond
        t0 = Date.now()
      }
      const tick = _ => {
        if(!started) return
        const t = Date.now()
        const dt = t - t0
        const dti = dt / kbtime
        const theta = dti * 3.141592654
        const dx = ((1+Math.cos(theta))/2) * kbrange
        if( kbdirection == "vertical" ) {
          img.style.left = "0px"
          img.style.top = `-${dx}px`
        } else {
          img.style.top = "0px"
          img.style.left = `-${dx}px`
        }
      }
      window.addEventListener("resize",compute)
      let started = false
      change()
      setInterval(tick,(1000/fps)|0)
      setInterval(change,changetime)
    })
  </script>
  <style>
    body {
      margin: 20px;
      padding: 0px;
      overflow: hidden;
    }
    img {
      margin: 0px;
      padding: 0px;
      position: absolute;
      top: 0px;
      left: 0px;
    }
  </style>
</head>
<body>
</body>
</html>

HTML Client With Improved Ken Burns effect

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Slideshow</title>
  <script>
    addEventListener("load",_ => {
      const q = (x,y=document) => y.querySelector(x)
      const qq = (x,y=document) => Array.from(y.querySelectorAll(x))
      const img = new Image()
      const fps = 60
      const secs = 1000
      const kbtime = 10 * secs
      const changetime = kbtime
      const ease_linear = x => x
      const ease_cos = x => 0.5*(1 - Math.cos(x*3.141592654))
      let ease = ease_cos
      document.body.append(img)
      let iw, ih, ir
      let ow, oh
      let ww, wh, wr
      let rect1, rect2
      let kbdirection, kbrange, kbinc
      let t0
      let started = false
      const lerp = (a,b,t) => a + t * (b - a)
      const change = _ => {
        fetch("/next").then(response => response.json()).then(data => {
          const newimg = new Image()
          newimg.src = data.path
          newimg.onload = _ => {
            img.src = newimg.src
            iw = newimg.naturalWidth
            ih = newimg.naturalHeight
            compute()
          }
          newimg.src = data.path
        })
      }
      const compute = _ => {
        started = true
        ww = window.innerWidth
        wh = window.innerHeight
        wr = ww/wh
        ir = iw/ih

        // basically we want to maximally fit a rectangle
        // with the same aspect ration as the screen
        // inside the source image rectangle,
        // then apply the scale factor to that
        // fitted rectangle
        // fw,fh should be width and height of
        // a rectangle with same aspect ration as screen
        // and fitting within the image
        let fr, fw, fh
        if( ir > wr ) {
          fh = ih
          fw = fh * wr
        } else {
          fw = iw
          fh = fw / wr
        }
        const rf = 0.3
        const s1 = 1 - rf * Math.random()
        const s2 = 1 - rf * Math.random()

        const w1 = (fw * s1)
        const w2 = (fw * s2)
        const h1 = (fh * s1)
        const h2 = (fh * s2)
        let xp1 = Math.random() * 0.5
        if( Math.random() > 0.5 ) { xp1 = 1 - xp1 }
        const xp2 = 1 - xp1
        let yp1 = Math.random() * 0.5
        if( Math.random() > 0.5 ) { yp1 = 1 - yp1 }
        const yp2 = 1 - yp1
        const x1 = xp1 * (iw-w1)
        const x2 = xp2 * (iw-w2)
        const y1 = yp1 * (ih-h1)
        const y2 = yp2 * (ih-h2)
        rect1 = { s1, x1, y1, w1, h1 }
        rect2 = { s2, x2, y2, w2, h2 }
        t0 = Date.now()
        tick()
      }
      const clamp = (a,b,x) => x < a ? a : (x > b ? b : x)
      const tick = _ => {
        if(!started) return
        const t1 = Date.now()
        const dt = clamp(0,kbtime,(t1 - t0))
        const t = ease(dt / kbtime) // t == lerp parameter
        const { s1, x1, y1, w1, h1 } = rect1
        const { s2, x2, y2, w2, h2 } = rect2
        const x = lerp(x1,x2,t)
        const y = lerp(y1,y2,t)
        const w = lerp(w1,w2,t)
        const h = lerp(h1,h2,t)

        const mx = ww / w
        const my = wh / h
        const ox = (-x * mx)
        const oy = (-y * my)

        const ow = (iw*mx)
        const oh = (ih*my)

        img.style.left = `${ox}px`
        img.style.top = `${oy}px`
        img.style.width = `${ow}px`
        img.style.height = `${oh}px`
      }
      window.addEventListener("resize",compute)
      change()
      setInterval(tick,(1000/fps)|0)
      setInterval(change,changetime)
    })
  </script>
  <style>
    body {
      margin: 20px;
      padding: 0px;
      overflow: hidden;
    }
    img {
      margin: 0px;
      padding: 0px;
      position: absolute;
    }
  </style>
</head>
<body>
</body>
</html>