mirror of
https://github.com/Luzifer/streamdeck.git
synced 2024-12-20 09:41:19 +00:00
Extract text rendering to helper
Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
parent
fd6ac1e954
commit
9065b0ad0f
2 changed files with 148 additions and 112 deletions
|
@ -4,24 +4,17 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"image"
|
|
||||||
"image/color"
|
"image/color"
|
||||||
"image/draw"
|
|
||||||
_ "image/jpeg"
|
_ "image/jpeg"
|
||||||
_ "image/png"
|
_ "image/png"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Luzifer/go_helpers/v2/env"
|
"github.com/Luzifer/go_helpers/v2/env"
|
||||||
"github.com/golang/freetype"
|
|
||||||
"github.com/golang/freetype/truetype"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.org/x/image/font"
|
|
||||||
"golang.org/x/image/math/fixed"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -34,13 +27,10 @@ type displayElementExec struct {
|
||||||
|
|
||||||
func (d displayElementExec) Display(ctx context.Context, idx int, attributes map[string]interface{}) error {
|
func (d displayElementExec) Display(ctx context.Context, idx int, attributes map[string]interface{}) error {
|
||||||
var (
|
var (
|
||||||
err error
|
err error
|
||||||
img draw.Image = image.NewRGBA(image.Rect(0, 0, sd.IconSize(), sd.IconSize()))
|
imgRenderer = newTextOnImageRenderer()
|
||||||
)
|
)
|
||||||
|
|
||||||
// Initialize black image
|
|
||||||
draw.Draw(img, img.Bounds(), image.NewUniform(color.RGBA{0x0, 0x0, 0x0, 0xff}), image.ZP, draw.Src)
|
|
||||||
|
|
||||||
// Initialize command
|
// Initialize command
|
||||||
cmd, ok := attributes["command"].([]interface{})
|
cmd, ok := attributes["command"].([]interface{})
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -98,14 +88,9 @@ func (d displayElementExec) Display(ctx context.Context, idx int, attributes map
|
||||||
|
|
||||||
// Initialize background
|
// Initialize background
|
||||||
if filename, ok := attributes["image"].(string); ok {
|
if filename, ok := attributes["image"].(string); ok {
|
||||||
bgi, err := d.getImageFromDisk(filename)
|
if err = imgRenderer.DrawBackgroundFromFile(filename); err != nil {
|
||||||
if err != nil {
|
return errors.Wrap(err, "Unable to draw background from disk")
|
||||||
return errors.Wrap(err, "Unable to get image from disk")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bgi = autoSizeImage(bgi, sd.IconSize())
|
|
||||||
|
|
||||||
draw.Draw(img, img.Bounds(), bgi, image.ZP, draw.Src)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize color
|
// Initialize color
|
||||||
|
@ -140,21 +125,8 @@ func (d displayElementExec) Display(ctx context.Context, idx int, attributes map
|
||||||
border = v
|
border = v
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render text
|
|
||||||
f, err := d.loadFont()
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "Unable to load font")
|
|
||||||
}
|
|
||||||
|
|
||||||
c := freetype.NewContext()
|
|
||||||
c.SetClip(img.Bounds())
|
|
||||||
c.SetDPI(72)
|
|
||||||
c.SetDst(img)
|
|
||||||
c.SetFont(f)
|
|
||||||
c.SetHinting(font.HintingNone)
|
|
||||||
|
|
||||||
if strings.TrimSpace(attributes["text"].(string)) != "" {
|
if strings.TrimSpace(attributes["text"].(string)) != "" {
|
||||||
if err = d.drawText(c, strings.TrimSpace(attributes["text"].(string)), textColor, fontsize, border); err != nil {
|
if err = imgRenderer.DrawBigText(strings.TrimSpace(attributes["text"].(string)), fontsize, border, textColor); err != nil {
|
||||||
return errors.Wrap(err, "Unable to render text")
|
return errors.Wrap(err, "Unable to render text")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -168,7 +140,7 @@ func (d displayElementExec) Display(ctx context.Context, idx int, attributes map
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.Wrap(sd.FillImage(idx, img), "Unable to set image")
|
return errors.Wrap(sd.FillImage(idx, imgRenderer.GetImage()), "Unable to set image")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d displayElementExec) NeedsLoop(attributes map[string]interface{}) bool {
|
func (d displayElementExec) NeedsLoop(attributes map[string]interface{}) bool {
|
||||||
|
@ -206,81 +178,3 @@ func (d *displayElementExec) StopLoopDisplay() error {
|
||||||
d.running = false
|
d.running = false
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (displayElementExec) drawText(c *freetype.Context, text string, textColor color.Color, fontsize float64, border int) error {
|
|
||||||
c.SetSrc(image.NewUniform(color.RGBA{0x0, 0x0, 0x0, 0x0})) // Transparent for text size guessing
|
|
||||||
|
|
||||||
textLines := strings.Split(text, "\n")
|
|
||||||
|
|
||||||
for {
|
|
||||||
c.SetFontSize(fontsize)
|
|
||||||
|
|
||||||
var maxX fixed.Int26_6
|
|
||||||
for _, tl := range textLines {
|
|
||||||
ext, err := c.DrawString(tl, freetype.Pt(0, 0))
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "Unable to measure text")
|
|
||||||
}
|
|
||||||
if ext.X > maxX {
|
|
||||||
maxX = ext.X
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if int(float64(maxX)/64) > sd.IconSize()-2*border || (int(c.PointToFixed(fontsize)/64))*len(textLines)+(len(textLines)-1)*2 > sd.IconSize()-2*border {
|
|
||||||
fontsize -= 2
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
yTotal = (int(c.PointToFixed(fontsize)/64))*len(textLines) + len(textLines)*2
|
|
||||||
yLineTop = int(float64(sd.IconSize())/2.0 - float64(yTotal)/2.0)
|
|
||||||
)
|
|
||||||
|
|
||||||
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))
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "Unable to measure text")
|
|
||||||
}
|
|
||||||
|
|
||||||
c.SetSrc(image.NewUniform(textColor))
|
|
||||||
|
|
||||||
xcenter := (float64(sd.IconSize()-2*border) / 2.0) - (float64(int(float64(ext.X)/64)) / 2.0) + float64(border)
|
|
||||||
ylower := yLineTop + int(c.PointToFixed(fontsize)/64)
|
|
||||||
|
|
||||||
if _, err = c.DrawString(tl, freetype.Pt(int(xcenter), int(ylower))); err != nil {
|
|
||||||
return errors.Wrap(err, "Unable to draw text")
|
|
||||||
}
|
|
||||||
|
|
||||||
yLineTop += int(c.PointToFixed(fontsize)/64) + 2
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (displayElementExec) getImageFromDisk(filename string) (image.Image, error) {
|
|
||||||
f, err := os.Open(filename)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "Unable to open image")
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
img, _, err := image.Decode(f)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "Umable to decode image")
|
|
||||||
}
|
|
||||||
|
|
||||||
return img, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (displayElementExec) loadFont() (*truetype.Font, error) {
|
|
||||||
fontRaw, err := ioutil.ReadFile(userConfig.RenderFont)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "Unable to read font file")
|
|
||||||
}
|
|
||||||
|
|
||||||
return truetype.Parse(fontRaw)
|
|
||||||
}
|
|
||||||
|
|
142
cmd/streamdeck/helper_displayText.go
Normal file
142
cmd/streamdeck/helper_displayText.go
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"image/draw"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/golang/freetype"
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"golang.org/x/image/font"
|
||||||
|
"golang.org/x/image/math/fixed"
|
||||||
|
)
|
||||||
|
|
||||||
|
type textOnImageRenderer struct {
|
||||||
|
img draw.Image
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTextOnImageRenderer() *textOnImageRenderer {
|
||||||
|
// Create new black image in icon size
|
||||||
|
var img draw.Image = image.NewRGBA(image.Rect(0, 0, sd.IconSize(), sd.IconSize()))
|
||||||
|
draw.Draw(img, img.Bounds(), image.NewUniform(color.RGBA{0x0, 0x0, 0x0, 0xff}), image.ZP, draw.Src)
|
||||||
|
|
||||||
|
return &textOnImageRenderer{
|
||||||
|
img: img,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *textOnImageRenderer) DrawBackground(bgi image.Image) {
|
||||||
|
bgi = autoSizeImage(bgi, sd.IconSize())
|
||||||
|
draw.Draw(t.img, t.img.Bounds(), bgi, image.ZP, draw.Src)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *textOnImageRenderer) DrawBackgroundFromFile(filename string) error {
|
||||||
|
bgi, err := t.getImageFromDisk(filename)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "Unable to get image from disk")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.DrawBackground(bgi)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *textOnImageRenderer) DrawBigText(text string, fontSizeHint float64, border int, textColor color.Color) error {
|
||||||
|
// Render text
|
||||||
|
f, err := t.loadFont()
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "Unable to load font")
|
||||||
|
}
|
||||||
|
|
||||||
|
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, fontSizeHint, border)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
c.SetSrc(image.NewUniform(color.RGBA{0x0, 0x0, 0x0, 0x0})) // Transparent for text size guessing
|
||||||
|
|
||||||
|
textLines := strings.Split(text, "\n")
|
||||||
|
|
||||||
|
for {
|
||||||
|
c.SetFontSize(fontsize)
|
||||||
|
|
||||||
|
var maxX fixed.Int26_6
|
||||||
|
for _, tl := range textLines {
|
||||||
|
ext, err := c.DrawString(tl, freetype.Pt(0, 0))
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "Unable to measure text")
|
||||||
|
}
|
||||||
|
if ext.X > maxX {
|
||||||
|
maxX = ext.X
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if int(float64(maxX)/64) > sd.IconSize()-2*border || (int(c.PointToFixed(fontsize)/64))*len(textLines)+(len(textLines)-1)*2 > sd.IconSize()-2*border {
|
||||||
|
fontsize -= 2
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
yTotal = (int(c.PointToFixed(fontsize)/64))*len(textLines) + len(textLines)*2
|
||||||
|
yLineTop = int(float64(sd.IconSize())/2.0 - float64(yTotal)/2.0)
|
||||||
|
)
|
||||||
|
|
||||||
|
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))
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "Unable to measure text")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.SetSrc(image.NewUniform(textColor))
|
||||||
|
|
||||||
|
xcenter := (float64(sd.IconSize()-2*border) / 2.0) - (float64(int(float64(ext.X)/64)) / 2.0) + float64(border)
|
||||||
|
ylower := yLineTop + int(c.PointToFixed(fontsize)/64)
|
||||||
|
|
||||||
|
if _, err = c.DrawString(tl, freetype.Pt(int(xcenter), int(ylower))); err != nil {
|
||||||
|
return errors.Wrap(err, "Unable to draw text")
|
||||||
|
}
|
||||||
|
|
||||||
|
yLineTop += int(c.PointToFixed(fontsize)/64) + 2
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (textOnImageRenderer) getImageFromDisk(filename string) (image.Image, error) {
|
||||||
|
f, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "Unable to open image")
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
img, _, err := image.Decode(f)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "Umable to decode image")
|
||||||
|
}
|
||||||
|
|
||||||
|
return img, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (textOnImageRenderer) loadFont() (*truetype.Font, error) {
|
||||||
|
fontRaw, err := ioutil.ReadFile(userConfig.RenderFont)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "Unable to read font file")
|
||||||
|
}
|
||||||
|
|
||||||
|
return truetype.Parse(fontRaw)
|
||||||
|
}
|
Loading…
Reference in a new issue