moved scaling of images from a fixed value to a query parameter
This commit is contained in:
parent
2dcb96399d
commit
c6b268c8ab
6 changed files with 37 additions and 103 deletions
3
.vscode/launch.json
vendored
3
.vscode/launch.json
vendored
|
@ -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"
|
||||||
|
|
11
README.md
11
README.md
|
@ -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:**
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
|
||||||
}
|
|
Loading…
Reference in a new issue