moved scaling of images from a fixed value to a query parameter

This commit is contained in:
Daniel Sommer 2023-11-29 16:27:23 +01:00
parent 2dcb96399d
commit c6b268c8ab
6 changed files with 37 additions and 103 deletions

3
.vscode/launch.json vendored
View file

@ -10,9 +10,8 @@
"env":{ "env":{
"SLIDESHOW_ADDRESS": "0.0.0.0", "SLIDESHOW_ADDRESS": "0.0.0.0",
"SLIDESHOW_PORT": "3000", "SLIDESHOW_PORT": "3000",
"SLIDESHOW_DIRECTORY": "/mnt/images", "SLIDESHOW_DIRECTORY": "/home/velvettear/images",
"SLIDESHOW_SCANINTERVAL": "600", "SLIDESHOW_SCANINTERVAL": "600",
"SLIDESHOW_RESOLUTION": "600x1024",
"SLIDESHOW_LOGLEVEL": "debug", "SLIDESHOW_LOGLEVEL": "debug",
"SLIDESHOW_PALETTE_ALGORITHM": "wsm", "SLIDESHOW_PALETTE_ALGORITHM": "wsm",
"SLIDESHOW_PALETTE_COLORS": "16" "SLIDESHOW_PALETTE_COLORS": "16"

View file

@ -4,7 +4,13 @@ a simple web server serving (scaled) images and color palettes for [slideshow](h
## requirements ## requirements
- [ImageMagick](https://imagemagick.org/) (optional) - [ImageMagick](https://imagemagick.org/) (optional; for scaling)
## endpoints
- `/`: request the name of a random image
- `/image/<name-of-an-image.jpeg>[?resolution=1920x1080]`: request the named image (in the specified resolution)
- `/palette/<name-of-an-image.jpeg>`: request the color palette of the named image
## configuration ## configuration
@ -16,13 +22,12 @@ configuration is entirely done via environment variables.
| SLIDESHOW_PORT | 3000 | the port of the web server | | SLIDESHOW_PORT | 3000 | the port of the web server |
| 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_RESOLUTION | | the resolution to which images are scaled (i.e. 1920x1080) |
| SLIDESHOW_PALETTE_ALGORITHM | "wsm" | the algorithm used to generate the color palette | | SLIDESHOW_PALETTE_ALGORITHM | "wsm" | the algorithm used to generate the color palette |
| SLIDESHOW_PALETTE_COLORS | 16 | the amount of colors generated | | SLIDESHOW_PALETTE_COLORS | 16 | the amount of colors generated |
| SLIDESHOW_LOGLEVEL | "info" | the log level | | SLIDESHOW_LOGLEVEL | "info" | the log level |
**note:** **note:**
if `SLIDESHOW_RESOLUTION` is unset or imagemagick's `convert` command is not in your `$PATH` the images will served as they are (without any scaling). if imagemagick's `convert` command is not in your `$PATH` the images will **not** be scaled.
**available log levels:** **available log levels:**

View file

@ -52,7 +52,7 @@ func serveImage(writer http.ResponseWriter, request *http.Request) {
response.send(writer) response.send(writer)
return return
} }
streamImage(writer, image) streamImage(writer, image, request.URL.Query().Get("resolution"))
} }
// request url: '/palette' - serve the color palette of an image // request url: '/palette' - serve the color palette of an image

View file

@ -17,9 +17,9 @@ import (
) )
// central streaming method for images // central streaming method for images
func streamImage(writer http.ResponseWriter, image string) { func streamImage(writer http.ResponseWriter, image string, resolution string) {
if config.IsResolutionSet() { if config.ConvertAvailable && config.IsValidResolution(resolution) {
streamScaledImage(writer, image) streamScaledImage(writer, image, resolution)
return return
} }
streamUnscaledImage(writer, image) streamUnscaledImage(writer, image)
@ -77,10 +77,10 @@ func streamUnscaledImage(writer http.ResponseWriter, image string) {
} }
// scale and stream an image // scale and stream an image
func streamScaledImage(writer http.ResponseWriter, image string) { func streamScaledImage(writer http.ResponseWriter, image string, resolution string) {
timestamp := time.Now().UnixMilli() timestamp := time.Now().UnixMilli()
var response response var response response
cmd := exec.Command("convert", image, "-resize", config.Resolution+"^", "-gravity", "center", "-extent", config.Resolution, "-") cmd := exec.Command("convert", image, "-resize", resolution+"^", "-gravity", "center", "-extent", resolution, "-")
stdout, stdoutError := cmd.StdoutPipe() stdout, stdoutError := cmd.StdoutPipe()
stderr, stderrError := cmd.StderrPipe() stderr, stderrError := cmd.StderrPipe()
cmd.Start() cmd.Start()
@ -120,5 +120,5 @@ func streamScaledImage(writer http.ResponseWriter, image string) {
} }
} }
cmd.Wait() cmd.Wait()
loggo.InfoTimed("successfully streamed scaled image '"+image+"'", timestamp, "size: "+internal.FormatBytes(written), "resolution: "+config.Resolution) loggo.InfoTimed("successfully streamed scaled image '"+image+"'", timestamp, "size: "+internal.FormatBytes(written), "resolution: "+resolution)
} }

View file

@ -14,10 +14,10 @@ var ServerAddress string
var ServerPort int var ServerPort int
var Directory string var Directory string
var ScanInterval time.Duration var ScanInterval time.Duration
var Resolution string
var PaletteAlgorithm int var PaletteAlgorithm int
var PaletteColors int var PaletteColors int
var BufferSize int var BufferSize int
var ConvertAvailable bool
// initialize the config // initialize the config
func Initialize() { func Initialize() {
@ -48,21 +48,6 @@ func Initialize() {
tmpInt = 60 tmpInt = 60
} }
ScanInterval = time.Duration(tmpInt) * time.Second ScanInterval = time.Duration(tmpInt) * time.Second
Resolution = os.Getenv("SLIDESHOW_RESOLUTION")
if len(Resolution) > 0 {
width, height, found := strings.Cut(Resolution, "x")
if !found {
loggo.Fatal("encountered an error parsing the configured resolution, make sure to specify the format like '1920x1080'")
}
_, error = strconv.Atoi(width)
if error != nil {
loggo.Fatal("encountered an error parsing the configured width '" + width + "'")
}
_, error = strconv.Atoi(height)
if error != nil {
loggo.Fatal("encountered an error parsing the configured height '" + height + "'")
}
}
tmpString := os.Getenv("SLIDESHOW_PALETTE_ALGORITHM") tmpString := os.Getenv("SLIDESHOW_PALETTE_ALGORITHM")
if strings.ToLower(tmpString) == "wu" { if strings.ToLower(tmpString) == "wu" {
PaletteAlgorithm = 0 PaletteAlgorithm = 0
@ -79,23 +64,33 @@ func Initialize() {
tmpInt = 4096 tmpInt = 4096
} }
BufferSize = tmpInt BufferSize = tmpInt
if IsResolutionSet() { checkConvertCommand()
checkForConvert()
}
} }
// check if 'convert' is available // check if 'convert' is available
func checkForConvert() { func checkConvertCommand() {
cmd := exec.Command("convert") cmd := exec.Command("which", "convert")
error := cmd.Run() error := cmd.Run()
ConvertAvailable = error == nil
if error == nil { if error == nil {
return return
} }
Resolution = "" loggo.Warning("could not find imagemagick's 'convert' command, scaling is unavailable", error.Error())
loggo.Warning("could not find imagemagick's 'convert' command, no scaling will be done", error.Error())
} }
// check if a resolution has been specified // check if the given string is a valid resolution
func IsResolutionSet() bool { func IsValidResolution(resolution string) bool {
return len(Resolution) > 0 if len(resolution) <= 0 {
return false
}
width, height, found := strings.Cut(resolution, "x")
if !found {
return false
}
_, error := strconv.Atoi(width)
if error != nil {
return false
}
_, error = strconv.Atoi(height)
return error == nil
} }

View file

@ -1,65 +0,0 @@
package internal
import (
"bytes"
"fmt"
"image"
"image/color"
"strconv"
"time"
"git.velvettear.de/velvettear/loggo"
"git.velvettear.de/velvettear/slideshow-api/internal/config"
color_thief "github.com/kennykarnama/color-thief"
)
// extract the given amount of dominant colors of raw image bytes
func GetColorPalette(data []byte) (string, error) {
var palette string
timestamp := time.Now().UnixMilli()
amount := config.PaletteColors
img, _, error := image.Decode(bytes.NewReader(data))
if error != nil {
loggo.ErrorTimed("encountered an error decoding the provided raw image bytes to an image", timestamp, error.Error())
return palette, error
}
colors, error := color_thief.GetPalette(img, amount, config.PaletteAlgorithm)
if error != nil {
loggo.ErrorTimed("encountered an error generating the color palette from raw image bytes", timestamp, "colors: "+strconv.Itoa(amount))
return palette, error
} else {
loggo.DebugTimed("generated color palette from raw image bytes", timestamp, "colors: "+strconv.Itoa(amount))
}
for index, color := range colors {
if index > 0 {
palette += "\n"
}
tmp := strconv.Itoa(index)
if len(tmp) < 2 {
tmp = "0" + tmp
}
tmp = "SLIDESHOW_COLOR" + tmp
palette += tmp + "=\"" + rgbToHex(color) + "\""
}
return palette, nil
}
// extract the given amount of dominant colors of an image
func getColorPalette(image string) ([]color.Color, error) {
var colors []color.Color
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
func rgbToHex(color color.Color) string {
r, g, b, _ := color.RGBA()
return fmt.Sprintf("#%02x%02x%02x", uint8(r>>8), uint8(g>>8), uint8(b>>8))
}