implemented variable 'SLIDESHOW_TMPFILE' to lower memory consumption

This commit is contained in:
Daniel Sommer 2023-11-27 12:28:25 +01:00
parent acc7d1513b
commit 5eb3fd0ea1
6 changed files with 119 additions and 17 deletions

3
.vscode/launch.json vendored
View file

@ -11,7 +11,8 @@
"SLIDESHOW_INTERVAL": "10", "SLIDESHOW_INTERVAL": "10",
"SLIDESHOW_DIRECTORY": "/home/velvettear/images", "SLIDESHOW_DIRECTORY": "/home/velvettear/images",
"SLIDESHOW_SCANINTERVAL": "10", "SLIDESHOW_SCANINTERVAL": "10",
"SLIDESHOW_RESOLUTION": "600x1024", "SLIDESHOW_TMPFILE": "/tmp/.slideshow.img",
"SLIDESHOW_RESOLUTION": "",
"SLIDESHOW_LOGLEVEL": "debug", "SLIDESHOW_LOGLEVEL": "debug",
"SLIDESHOW_PALETTE": "/tmp/.slideshow.palette", "SLIDESHOW_PALETTE": "/tmp/.slideshow.palette",
"SLIDESHOW_PALETTE_ALGORITHM": "wsm", "SLIDESHOW_PALETTE_ALGORITHM": "wsm",

View file

@ -15,7 +15,8 @@ configuration is entirely done via environment variables.
| --------------------------- | ----------------- | -----------------------------------------------------------| | --------------------------- | ----------------- | -----------------------------------------------------------|
| SLIDESHOW_INTERVAL | 60 | the interval of the slideshow in seconds | | SLIDESHOW_INTERVAL | 60 | the interval of the slideshow in seconds |
| SLIDESHOW_DIRECTORY | "$HOME" | path to a directory containing images | | SLIDESHOW_DIRECTORY | "$HOME" | path to a directory containing images |
| SLIDESHOW_SCANINTERVAL | 60 | the interval for directory scans in seconds | SLIDESHOW_SCANINTERVAL | 60 | the interval for directory scans in seconds |
| SLIDESHOW_TMPFILE | | path to a temporary file |
| SLIDESHOW_RESOLUTION | | the resolution to which images are scaled (i.e. 1920x1080) | | SLIDESHOW_RESOLUTION | | the resolution to which images are scaled (i.e. 1920x1080) |
| SLIDESHOW_PALETTE | | path to a file where the color palette will be stored | | SLIDESHOW_PALETTE | | path to a file where the color palette will be stored |
| SLIDESHOW_PALETTE_ALGORITHM | "wsm" | the algorithm used to generate the color palette | | SLIDESHOW_PALETTE_ALGORITHM | "wsm" | the algorithm used to generate the color palette |
@ -23,7 +24,8 @@ configuration is entirely done via environment variables.
| SLIDESHOW_LOGLEVEL | "info" | the log level | | SLIDESHOW_LOGLEVEL | "info" | the log level |
**note:** **note:**
if no resolution is set the images will be displayed as they are (without any scaling). - if `SLIDESHOW_RESOLUTION` is unset the images will be displayed as they are (without any scaling).
- if `SLIDESHOW_TMPFILE` is set a temporary file will be used instead of keeping the image internally buffered. this could be useful on systems with limited ram.
**available log levels:** **available log levels:**

View file

@ -12,6 +12,7 @@ import (
var Interval time.Duration var Interval time.Duration
var Directory string var Directory string
var ScanInterval time.Duration var ScanInterval time.Duration
var TmpFile string
var Resolution string var Resolution string
var PaletteFile string var PaletteFile string
var PaletteAlgorithm int var PaletteAlgorithm int
@ -45,6 +46,7 @@ func Initialize() {
tmpInt = 60 tmpInt = 60
} }
ScanInterval = time.Duration(tmpInt) * time.Second ScanInterval = time.Duration(tmpInt) * time.Second
TmpFile = os.Getenv("SLIDESHOW_TMPFILE")
Resolution = os.Getenv("SLIDESHOW_RESOLUTION") Resolution = os.Getenv("SLIDESHOW_RESOLUTION")
if len(Resolution) > 0 { if len(Resolution) > 0 {
width, height, found := strings.Cut(Resolution, "x") width, height, found := strings.Cut(Resolution, "x")
@ -83,3 +85,8 @@ func IsResolutionSet() bool {
func IsPaletteSet() bool { func IsPaletteSet() bool {
return len(PaletteFile) > 0 return len(PaletteFile) > 0
} }
// check if a temporary file has been specified
func IsTemporaryFileSet() bool {
return len(TmpFile) > 0
}

View file

@ -62,6 +62,23 @@ func getColorPaletteRaw(data []byte) ([]color.Color, error) {
return colors, error return colors, error
} }
// extract the given amount of dominant colors of an image
func getColorPalette(image string) ([]color.Color, error) {
var colors []color.Color
if !config.IsPaletteSet() {
return colors, nil
}
timestamp := time.Now().UnixMilli()
amount := config.PaletteColors
colors, error := color_thief.GetPaletteFromFile(image, amount, config.PaletteAlgorithm)
if error != nil {
loggo.ErrorTimed("encountered an error generating the color palette from image", timestamp, "image: "+image, "colors: "+strconv.Itoa(amount))
} else {
loggo.DebugTimed("generated color palette from image", timestamp, "image: "+image, "colors: "+strconv.Itoa(amount))
}
return colors, error
}
// parse rgb color values to hex // parse rgb color values to hex
func rgbToHex(color color.Color) string { func rgbToHex(color color.Color) string {
r, g, b, _ := color.RGBA() r, g, b, _ := color.RGBA()

View file

@ -4,7 +4,6 @@ import (
"errors" "errors"
"io" "io"
"os/exec" "os/exec"
"strconv"
"strings" "strings"
"time" "time"
@ -13,9 +12,12 @@ import (
) )
// scale an image // scale an image
func scale(image string) ([]byte, error) { func scale(image string, output string) ([]byte, error) {
timestamp := time.Now().UnixMilli() timestamp := time.Now().UnixMilli()
cmd := exec.Command("convert", image, "-resize", config.Resolution+"^", "-gravity", "center", "-extent", config.Resolution, "-") if len(output) == 0 {
output = "-"
}
cmd := exec.Command("convert", image, "-resize", config.Resolution+"^", "-gravity", "center", "-extent", config.Resolution, output)
stdout, stdoutError := cmd.StdoutPipe() stdout, stdoutError := cmd.StdoutPipe()
stderr, stderrError := cmd.StderrPipe() stderr, stderrError := cmd.StderrPipe()
var data []byte var data []byte
@ -39,6 +41,6 @@ func scale(image string) ([]byte, error) {
if len(errorMessage) > 0 { if len(errorMessage) > 0 {
return data, errors.New(errorMessage) return data, errors.New(errorMessage)
} }
loggo.DebugTimed("successfully scaled image", timestamp, "image: "+image, "resolution: "+config.Resolution, "size: "+strconv.Itoa(len(data))) loggo.DebugTimed("successfully scaled image", timestamp, "image: "+image, "resolution: "+config.Resolution)
return data, nil return data, nil
} }

View file

@ -43,30 +43,75 @@ func Start() {
} }
} }
if config.IsResolutionSet() { if config.IsResolutionSet() {
tmp, error := scale(image) tmp, error := scale(image, config.TmpFile)
if error != nil { if error != nil {
loggo.Error("encountered an error scaling an image", "image: "+image, error.Error()) loggo.Error("encountered an error scaling an image", "image: "+image, error.Error())
continue continue
} }
data = tmp data = tmp
} else { } else {
tmp, error := os.ReadFile(image) if !config.IsTemporaryFileSet() {
if error != nil { tmp, error := os.ReadFile(image)
loggo.Error("encountered an erro reading an image", "image: "+image, error.Error()) if error != nil {
continue loggo.Error("encountered an error reading an image", "image: "+image, error.Error())
continue
}
data = tmp
} else {
file, error := os.Open(image)
if error != nil {
loggo.Error("encountered an error opening an image", "image: "+image, error.Error())
continue
}
defer file.Close()
tmpFile, error := os.Create(config.TmpFile)
if error != nil {
loggo.Error("encountered an error opening the temporary file", "path: "+config.TmpFile, error.Error())
continue
}
defer tmpFile.Close()
var errorMessage string
buffer := make([]byte, 4096)
for {
read, error := file.Read(buffer)
if error != nil && error != io.EOF {
errorMessage = error.Error()
break
}
if read == 0 {
break
}
_, error = tmpFile.Write(buffer[:read])
if error != nil {
errorMessage = error.Error()
break
}
}
if len(errorMessage) > 0 {
loggo.Error("encountered an error reading an image", "image: "+image, errorMessage)
continue
}
} }
data = tmp
} }
palette, _ = getColorPaletteRaw(data) if config.IsTemporaryFileSet() {
palette, _ = getColorPalette(config.TmpFile)
} else {
palette, _ = getColorPaletteRaw(data)
}
loopTime = time.Since(loopTimestamp) loopTime = time.Since(loopTimestamp)
if sleepTime > 0 { if sleepTime > 0 {
loggo.Debug("sleeping for " + strconv.FormatInt(sleepTime.Milliseconds(), 10) + "ms before next image will be displayed...") loggo.Debug("sleeping for " + strconv.FormatInt(sleepTime.Milliseconds(), 10) + "ms before next image will be displayed...")
time.Sleep(sleepTime) time.Sleep(sleepTime)
} }
go exportPalette(palette) go exportPalette(palette)
error := setBackground(data) var err error
if error != nil { if config.IsTemporaryFileSet() {
loggo.Error("encountered an error setting the background image", error.Error()) err = setBackgroundImage(config.TmpFile)
} else {
err = setBackground(data)
}
if err != nil {
loggo.Error("encountered an error setting the background image", err.Error())
} }
loggo.Info("set new background image", "image: "+image) loggo.Info("set new background image", "image: "+image)
sleepTime = config.Interval - loopTime sleepTime = config.Interval - loopTime
@ -113,3 +158,31 @@ func setBackground(data []byte) error {
} }
return nil return nil
} }
// set the background image via 'feh'
func setBackgroundImage(image string) error {
cmd := exec.Command("feh", "--no-fehbg", "--bg-fill", image)
stdout, stdoutError := cmd.StdoutPipe()
stderr, stderrError := cmd.StderrPipe()
cmd.Start()
if stdoutError != nil {
return stdoutError
}
if stderrError != nil {
return stderrError
}
_, stdoutError = io.ReadAll(stdout)
if stdoutError != nil {
return stdoutError
}
errorBytes, stderrError := io.ReadAll(stderr)
if stderrError != nil {
return stderrError
}
cmd.Wait()
error := strings.TrimSpace(string(errorBytes))
if len(error) > 0 {
return errors.New(error)
}
return nil
}