Skip to content

Commit

Permalink
fonts: fix race condition (#963)
Browse files Browse the repository at this point in the history
  • Loading branch information
matslina authored Nov 6, 2023
1 parent 1b3a5ec commit 58155a8
Show file tree
Hide file tree
Showing 7 changed files with 86 additions and 37 deletions.
19 changes: 12 additions & 7 deletions render/fonts.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ package render

import (
"encoding/base64"
"log"
"fmt"
"sync"

"github.com/zachomedia/go-bdf"
"golang.org/x/image/font"
)

var fontCache = map[string]font.Face{}
var fontMutex = &sync.Mutex{}

func GetFontList() []string {
fontNames := []string{}
Expand All @@ -20,26 +22,29 @@ func GetFontList() []string {
return fontNames
}

func GetFont(name string) font.Face {
func GetFont(name string) (font.Face, error) {
fontMutex.Lock()
defer fontMutex.Unlock()

if font, ok := fontCache[name]; ok {
return font
return font, nil
}

dataB64, ok := fontDataRaw[name]
if !ok {
log.Panicf("Unknown font '%s', the available fonts are: %v", name, GetFontList())
return nil, fmt.Errorf("unknown font '%s'", name)
}

data, err := base64.StdEncoding.DecodeString(dataB64)
if err != nil {
log.Panicf("couldn't decode %s: %s", name, err)
return nil, fmt.Errorf("decoding font '%s': %w", name, err)
}

f, err := bdf.Parse(data)
if err != nil {
log.Panicf("couldn't parse %s: %s", name, err)
return nil, fmt.Errorf("parsing font '%s': %w", name, err)
}

fontCache[name] = f.NewFace()
return fontCache[name]
return fontCache[name], nil
}
12 changes: 7 additions & 5 deletions render/stack.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ import (
//
// EXAMPLE BEGIN
// render.Stack(
// children=[
// render.Box(width=50, height=25, color="#911"),
// render.Text("hello there"),
// render.Box(width=4, height=32, color="#119"),
// ],
//
// children=[
// render.Box(width=50, height=25, color="#911"),
// render.Text("hello there"),
// render.Box(width=4, height=32, color="#119"),
// ],
//
// )
// EXAMPLE END
type Stack struct {
Expand Down
5 changes: 4 additions & 1 deletion render/text.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ func (t *Text) Init() error {
if t.Font == "" {
t.Font = DefaultFontFace
}
face := GetFont(t.Font)
face, err := GetFont(t.Font)
if err != nil {
return err
}

dc := gg.NewContext(0, 0)
dc.SetFontFace(face)
Expand Down
8 changes: 8 additions & 0 deletions render/text_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,3 +192,11 @@ func TestTextFonts(t *testing.T) {
assert.Equal(t, 17, w)
assert.Equal(t, 8, h)
}

func TestTextMissingFont(t *testing.T) {
text := &Text{
Content: "QqÖ!",
Font: "missing",
}
assert.Error(t, text.Init())
}
41 changes: 27 additions & 14 deletions render/wrappedtext.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"image/color"

"github.com/tidbyt/gg"

"golang.org/x/image/font"
)

// WrappedText draws multi-line text.
Expand All @@ -27,9 +29,11 @@ import (
// DOC(Align): Text Alignment
// EXAMPLE BEGIN
// render.WrappedText(
// content="this is a multi-line text string",
// width=50,
// color="#fa0",
//
// content="this is a multi-line text string",
// width=50,
// color="#fa0",
//
// )
// EXAMPLE END
type WrappedText struct {
Expand All @@ -42,13 +46,26 @@ type WrappedText struct {
LineSpacing int
Color color.Color
Align string

face font.Face
}

func (tw WrappedText) PaintBounds(bounds image.Rectangle, frameIdx int) image.Rectangle {
func (tw *WrappedText) Init() error {
if tw.Font == "" {
tw.Font = DefaultFontFace
}
face := GetFont(tw.Font)

face, err := GetFont(tw.Font)
if err != nil {
return err
}

tw.face = face

return nil
}

func (tw *WrappedText) PaintBounds(bounds image.Rectangle, frameIdx int) image.Rectangle {
// The bounds provided by user or parent widget
width := tw.Width
if width == 0 {
Expand All @@ -67,7 +84,7 @@ func (tw WrappedText) PaintBounds(bounds image.Rectangle, frameIdx int) image.Re
// NOTE: Can't use dc.MeasureMultilineString() here. It only
// deals with texts that have actual \n in them.
dc := gg.NewContext(width, 0)
dc.SetFontFace(face)
dc.SetFontFace(tw.face)
w := 0.0
h := 0.0
for _, line := range dc.WordWrap(tw.Content, float64(width)) {
Expand Down Expand Up @@ -98,11 +115,7 @@ func (tw WrappedText) PaintBounds(bounds image.Rectangle, frameIdx int) image.Re
return image.Rect(0, 0, width, height)
}

func (tw WrappedText) Paint(dc *gg.Context, bounds image.Rectangle, frameIdx int) {
if tw.Font == "" {
tw.Font = DefaultFontFace
}
face := GetFont(tw.Font)
func (tw *WrappedText) Paint(dc *gg.Context, bounds image.Rectangle, frameIdx int) {
// Text alignment
align := gg.AlignLeft
if tw.Align == "center" {
Expand All @@ -113,10 +126,10 @@ func (tw WrappedText) Paint(dc *gg.Context, bounds image.Rectangle, frameIdx int

width := tw.PaintBounds(bounds, frameIdx).Dx()

metrics := face.Metrics()
metrics := tw.face.Metrics()
descent := metrics.Descent.Floor()

dc.SetFontFace(face)
dc.SetFontFace(tw.face)
if tw.Color != nil {
dc.SetColor(tw.Color)
} else {
Expand All @@ -135,6 +148,6 @@ func (tw WrappedText) Paint(dc *gg.Context, bounds image.Rectangle, frameIdx int
)
}

func (tw WrappedText) FrameCount() int {
func (tw *WrappedText) FrameCount() int {
return 1
}
34 changes: 24 additions & 10 deletions render/wrappedtext_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import (
)

func TestWrappedTextWithBounds(t *testing.T) {
text := WrappedText{Content: "AB CD."}
text := &WrappedText{Content: "AB CD."}
assert.NoError(t, text.Init())

// Sufficient space to fit on single line
im := PaintWidget(text, image.Rect(0, 0, 25, 8), 0)
Expand Down Expand Up @@ -62,9 +63,10 @@ func TestWrappedTextWithBounds(t *testing.T) {
}, im))
}

func TestWrappedTextWithSize(t *testing.T) {
func TestWrappedTextWithsize(t *testing.T) {
// Weight and Height parameters override the bounds
text := WrappedText{Content: "AB CD.", Width: 7, Height: 12}
text := &WrappedText{Content: "AB CD.", Width: 7, Height: 12}
assert.NoError(t, text.Init())
im := PaintWidget(text, image.Rect(0, 0, 40, 40), 0)
assert.Equal(t, nil, checkImage([]string{
"....." + "..",
Expand All @@ -82,7 +84,8 @@ func TestWrappedTextWithSize(t *testing.T) {
}, im))

// Height can be overridden separately
text = WrappedText{Content: "AB CD.", Height: 12}
text = &WrappedText{Content: "AB CD.", Height: 12}
assert.NoError(t, text.Init())
im = PaintWidget(text, image.Rect(0, 0, 9, 40), 0)
assert.Equal(t, nil, checkImage([]string{
"....." + "....",
Expand All @@ -100,7 +103,8 @@ func TestWrappedTextWithSize(t *testing.T) {
}, im))

// Ditto for Width
text = WrappedText{Content: "AB CD.", Width: 3}
text = &WrappedText{Content: "AB CD.", Width: 3}
assert.NoError(t, text.Init())
im = PaintWidget(text, image.Rect(0, 0, 9, 5), 0)
assert.Equal(t, nil, checkImage([]string{
"...",
Expand All @@ -114,7 +118,8 @@ func TestWrappedTextWithSize(t *testing.T) {
func TestWrappedTextLineSpacing(t *testing.T) {

// Single pixel line space
text := WrappedText{Content: "AB CD.", LineSpacing: 1}
text := &WrappedText{Content: "AB CD.", LineSpacing: 1}
assert.NoError(t, text.Init())
im := PaintWidget(text, image.Rect(0, 0, 21, 16), 0)
assert.Equal(t, nil, checkImage([]string{
"....." + ".......",
Expand All @@ -136,7 +141,8 @@ func TestWrappedTextLineSpacing(t *testing.T) {
}, im))

// Add another one
text = WrappedText{Content: "AB CD.", LineSpacing: 2}
text = &WrappedText{Content: "AB CD.", LineSpacing: 2}
assert.NoError(t, text.Init())
im = PaintWidget(text, image.Rect(0, 0, 21, 16), 0)
assert.Equal(t, nil, checkImage([]string{
"....." + ".......",
Expand All @@ -160,7 +166,8 @@ func TestWrappedTextLineSpacing(t *testing.T) {

func TestWrappedTextAlignment(t *testing.T) {
// Default to left align.
text := WrappedText{Content: "AB CD."}
text := &WrappedText{Content: "AB CD."}
assert.NoError(t, text.Init())
im := PaintWidget(text, image.Rect(0, 0, 21, 16), 0)
assert.Equal(t, nil, checkImage([]string{
"......." + ".....",
Expand All @@ -182,7 +189,8 @@ func TestWrappedTextAlignment(t *testing.T) {
}, im))

// Right alignment.
text = WrappedText{Content: "AB CD.", Align: "right"}
text = &WrappedText{Content: "AB CD.", Align: "right"}
assert.NoError(t, text.Init())
im = PaintWidget(text, image.Rect(0, 0, 21, 16), 0)
assert.Equal(t, nil, checkImage([]string{
"......." + ".....",
Expand All @@ -204,7 +212,8 @@ func TestWrappedTextAlignment(t *testing.T) {
}, im))

// Center alignment.
text = WrappedText{Content: "AB CD.", Align: "center"}
text = &WrappedText{Content: "AB CD.", Align: "center"}
assert.NoError(t, text.Init())
im = PaintWidget(text, image.Rect(0, 0, 21, 16), 0)
assert.Equal(t, nil, checkImage([]string{
"......." + ".....",
Expand All @@ -225,3 +234,8 @@ func TestWrappedTextAlignment(t *testing.T) {
"......." + ".....",
}, im))
}

func TestWrappedTextMissingFont(t *testing.T) {
text := &WrappedText{Content: "AB CD.", Font: "missing"}
assert.Error(t, text.Init())
}
4 changes: 4 additions & 0 deletions runtime/modules/render_runtime/generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 58155a8

Please sign in to comment.