diff --git a/cmd/streamdeck/config.go b/cmd/streamdeck/config.go index a7c22d0..e149f0b 100644 --- a/cmd/streamdeck/config.go +++ b/cmd/streamdeck/config.go @@ -1,9 +1,16 @@ package main -import "time" +import ( + "time" +) type config struct { AutoReload bool `yaml:"auto_reload"` + CaptionBorder int `yaml:"caption_border"` + CaptionColor [4]int `yaml:"caption_color"` + CaptionFont string `yaml:"caption_font"` + CaptionFontSize float64 `yaml:"caption_font_size"` + CaptionPosition captionPosition `yaml:"caption_position"` DefaultBrightness int `yaml:"default_brightness"` DefaultPage string `yaml:"default_page"` DisplayOffTime time.Duration `yaml:"display_off_time"` @@ -31,3 +38,10 @@ func newConfig() config { AutoReload: true, } } + +type captionPosition string + +const ( + captionPositionBottom = "bottom" + captionPositionTop = "top" +) diff --git a/cmd/streamdeck/display_exec.go b/cmd/streamdeck/display_exec.go index f1c38d3..8bc889a 100644 --- a/cmd/streamdeck/display_exec.go +++ b/cmd/streamdeck/display_exec.go @@ -131,6 +131,12 @@ func (d displayElementExec) Display(ctx context.Context, idx int, attributes map } } + if caption, ok := attributes["caption"].(string); ok && strings.TrimSpace(caption) != "" { + if err = imgRenderer.DrawCaptionText(strings.TrimSpace(caption)); err != nil { + return errors.Wrap(err, "Unable to render caption") + } + } + if !d.running && d.NeedsLoop(attributes) { return nil } diff --git a/cmd/streamdeck/display_pulsevolume.go b/cmd/streamdeck/display_pulsevolume.go index 647b534..abf4cb0 100644 --- a/cmd/streamdeck/display_pulsevolume.go +++ b/cmd/streamdeck/display_pulsevolume.go @@ -6,6 +6,7 @@ import ( "context" "fmt" "image/color" + "strings" "time" "github.com/pkg/errors" @@ -112,6 +113,12 @@ func (d displayElementPulseVolume) Display(ctx context.Context, idx int, attribu return errors.Wrap(err, "Unable to draw text") } + if caption, ok := attributes["caption"].(string); ok && strings.TrimSpace(caption) != "" { + if err = img.DrawCaptionText(strings.TrimSpace(caption)); err != nil { + return errors.Wrap(err, "Unable to render caption") + } + } + if err = ctx.Err(); err != nil { // Page context was cancelled, do not draw return err diff --git a/cmd/streamdeck/helper_displayText.go b/cmd/streamdeck/helper_displayText.go index f0d1967..94889f5 100644 --- a/cmd/streamdeck/helper_displayText.go +++ b/cmd/streamdeck/helper_displayText.go @@ -15,6 +15,14 @@ import ( "golang.org/x/image/math/fixed" ) +type textDrawAnchor uint + +const ( + textDrawAnchorCenter textDrawAnchor = iota + textDrawAnchorBottom + textDrawAnchorTop +) + type textOnImageRenderer struct { img draw.Image } @@ -46,7 +54,7 @@ func (t *textOnImageRenderer) DrawBackgroundFromFile(filename string) error { func (t *textOnImageRenderer) DrawBigText(text string, fontSizeHint float64, border int, textColor color.Color) error { // Render text - f, err := t.loadFont() + f, err := t.loadFont(userConfig.RenderFont) if err != nil { return errors.Wrap(err, "Unable to load font") } @@ -58,12 +66,44 @@ func (t *textOnImageRenderer) DrawBigText(text string, fontSizeHint float64, bor c.SetFont(f) c.SetHinting(font.HintingNone) - return t.drawText(c, text, textColor, fontSizeHint, border) + return t.drawText(c, text, textColor, fontSizeHint, border, textDrawAnchorCenter) +} + +func (t *textOnImageRenderer) DrawCaptionText(text string) error { + // Render text + f, err := t.loadFont(userConfig.CaptionFont) + if err != nil { + return errors.Wrap(err, "Unable to load font") + } + + var textColor color.Color = color.RGBA{0xff, 0xff, 0xff, 0xff} + if userConfig.CaptionColor[3] != 0x0 { + textColor = color.RGBA{ + uint8(userConfig.CaptionColor[0]), + uint8(userConfig.CaptionColor[1]), + uint8(userConfig.CaptionColor[2]), + uint8(userConfig.CaptionColor[3]), + } + } + + var anchor = textDrawAnchorBottom + if userConfig.CaptionPosition == "top" { + anchor = textDrawAnchorTop + } + + c := freetype.NewContext() + c.SetClip(t.img.Bounds()) + c.SetDPI(72) + c.SetDst(t.img) + c.SetFont(f) + c.SetHinting(font.HintingNone) + + return t.drawText(c, text, textColor, userConfig.CaptionFontSize, userConfig.CaptionBorder, anchor) } func (t textOnImageRenderer) GetImage() image.Image { return t.img } -func (t *textOnImageRenderer) drawText(c *freetype.Context, text string, textColor color.Color, fontsize float64, border int) error { +func (t *textOnImageRenderer) drawText(c *freetype.Context, text string, textColor color.Color, fontsize float64, border int, anchor textDrawAnchor) error { c.SetSrc(image.NewUniform(color.RGBA{0x0, 0x0, 0x0, 0x0})) // Transparent for text size guessing textLines := strings.Split(text, "\n") @@ -92,9 +132,18 @@ func (t *textOnImageRenderer) drawText(c *freetype.Context, text string, textCol var ( yTotal = (int(c.PointToFixed(fontsize)/64))*len(textLines) + (len(textLines)-1)*2 - yLineTop = int(float64(sd.IconSize())/2.0 - float64(yTotal)/2.0) + yLineTop int ) + switch anchor { + case textDrawAnchorTop: + yLineTop = border + case textDrawAnchorCenter: + yLineTop = int(float64(sd.IconSize())/2.0 - float64(yTotal)/2.0) + case textDrawAnchorBottom: + yLineTop = sd.IconSize() - yTotal - border + } + for _, tl := range textLines { c.SetSrc(image.NewUniform(color.RGBA{0x0, 0x0, 0x0, 0x0})) // Transparent for text size guessing ext, err := c.DrawString(tl, freetype.Pt(0, 0)) @@ -132,8 +181,8 @@ func (textOnImageRenderer) getImageFromDisk(filename string) (image.Image, error return img, nil } -func (textOnImageRenderer) loadFont() (*truetype.Font, error) { - fontRaw, err := ioutil.ReadFile(userConfig.RenderFont) +func (textOnImageRenderer) loadFont(fontfile string) (*truetype.Font, error) { + fontRaw, err := ioutil.ReadFile(fontfile) if err != nil { return nil, errors.Wrap(err, "Unable to read font file") }