Live video on the AT Protocol
1package media
2
3import (
4 "bytes"
5 "fmt"
6 "image"
7 "image/color"
8 "image/draw"
9 "image/png"
10 "io"
11 "strings"
12
13 "github.com/golang/freetype"
14 "github.com/golang/freetype/truetype"
15 "golang.org/x/image/font"
16 "stream.place/streamplace/js/app"
17)
18
19type TextRenderer struct {
20 font *truetype.Font
21}
22
23func NewTextRenderer() (*TextRenderer, error) {
24 font, err := loadFont()
25 if err != nil {
26 return nil, err
27 }
28 return &TextRenderer{font: font}, nil
29}
30
31func loadFont() (*truetype.Font, error) {
32 entries, err := app.AllFiles.ReadDir("dist/assets/assets/fonts")
33 if err != nil {
34 return nil, err
35 }
36 var fontFile string
37 for _, entry := range entries {
38 if strings.HasPrefix(entry.Name(), "FiraCode-Medium") {
39 fontFile = fmt.Sprintf("dist/assets/assets/fonts/%s", entry.Name())
40 break
41 }
42 }
43 if fontFile == "" {
44 return nil, fmt.Errorf("could not find FiraCode-Medium font")
45 }
46 fd, err := app.AllFiles.Open(fontFile)
47 if err != nil {
48 return nil, err
49 }
50 fontBytes, err := io.ReadAll(fd)
51 if err != nil {
52 return nil, err
53 }
54 f, err := freetype.ParseFont(fontBytes)
55 if err != nil {
56 return nil, err
57 }
58 return f, nil
59}
60
61func (tr *TextRenderer) GenerateImage(textContent string, fgColorHex string, bgColorHex string, fontSize float64) ([]byte, error) {
62 fgColor := color.RGBA{0xff, 0xff, 0xff, 0xff}
63 if len(fgColorHex) == 7 {
64 _, err := fmt.Sscanf(fgColorHex, "#%02x%02x%02x", &fgColor.R, &fgColor.G, &fgColor.B)
65 if err != nil {
66 return nil, err
67 }
68 }
69
70 bgColor := color.RGBA{0x30, 0x0a, 0x24, 0xcc}
71 if len(bgColorHex) == 7 {
72 _, err := fmt.Sscanf(bgColorHex, "#%02x%02x%02x", &bgColor.R, &bgColor.G, &bgColor.B)
73 if err != nil {
74 return nil, err
75 }
76 }
77
78 code := strings.ReplaceAll(textContent, "\t", " ") // convert tabs into spaces
79 text := strings.Split(code, "\n") // split newlines into arrays
80
81 fg := image.NewUniform(fgColor)
82 bg := image.NewUniform(bgColor)
83 rgba := image.NewRGBA(image.Rect(0, 0, 540, 55))
84 draw.Draw(rgba, rgba.Bounds(), bg, image.Pt(0, 0), draw.Src)
85 c := freetype.NewContext()
86 c.SetDPI(72)
87 c.SetFont(tr.font)
88 c.SetFontSize(fontSize)
89 c.SetClip(rgba.Bounds())
90 c.SetDst(rgba)
91 c.SetSrc(fg)
92 c.SetHinting(font.HintingNone)
93
94 textXOffset := 10
95 textYOffset := 5 + int(c.PointToFixed(fontSize)>>6) // Note shift/truncate 6 bits first
96
97 pt := freetype.Pt(textXOffset, textYOffset)
98 for _, s := range text {
99 _, err := c.DrawString(strings.ReplaceAll(s, "\r", ""), pt)
100 if err != nil {
101 return nil, err
102 }
103 pt.Y += c.PointToFixed(fontSize * 1.5)
104 }
105
106 b := new(bytes.Buffer)
107 if err := png.Encode(b, rgba); err != nil {
108 return nil, err
109 }
110 return b.Bytes(), nil
111}