From 49a7b2e6f444bc606385207a4eee2c4b6fae7317 Mon Sep 17 00:00:00 2001 From: Walter Werner SCHNEIDER Date: Thu, 2 Sep 2021 22:20:38 +0300 Subject: [PATCH] text/shaper: lookup closest font by weight Signed-off-by: Walter Werner SCHNEIDER --- text/shaper.go | 48 ++++++++++++------ text/shaper_test.go | 118 ++++++++++++++++++++++++++++++++++++++++++++ text/text.go | 12 +++++ 3 files changed, 163 insertions(+), 15 deletions(-) create mode 100644 text/shaper_test.go diff --git a/text/shaper.go b/text/shaper.go index 0cfe3ce0..92af6fb5 100644 --- a/text/shaper.go +++ b/text/shaper.go @@ -56,24 +56,42 @@ func (c *Cache) lookup(font Font) *faceCache { } func (c *Cache) faceForStyle(font Font) *faceCache { - tf := c.faces[font] - if tf == nil { - font := font - font.Weight = Normal - tf = c.faces[font] + if closest, ok := c.closestFont(font); ok { + return c.faces[closest] } - if tf == nil { - font := font - font.Style = Regular - tf = c.faces[font] + font.Style = Regular + if closest, ok := c.closestFont(font); ok { + return c.faces[closest] } - if tf == nil { - font := font - font.Style = Regular - font.Weight = Normal - tf = c.faces[font] + return nil +} + +// closestFont returns the closest Font by weight, in case of equality the +// lighter weight will be returned. +func (c *Cache) closestFont(lookup Font) (Font, bool) { + if c.faces[lookup] != nil { + return lookup, true } - return tf + found := false + var match Font + for cf := range c.faces { + if cf.Typeface != lookup.Typeface || cf.Variant != lookup.Variant || cf.Style != lookup.Style { + continue + } + if !found { + found = true + match = cf + continue + } + cDist := weightDistance(lookup.Weight, cf.Weight) + mDist := weightDistance(lookup.Weight, match.Weight) + if cDist < mDist { + match = cf + } else if cDist == mDist && cf.Weight < match.Weight { + match = cf + } + } + return match, found } func NewCache(collection []FontFace) *Cache { diff --git a/text/shaper_test.go b/text/shaper_test.go new file mode 100644 index 00000000..9c96e7aa --- /dev/null +++ b/text/shaper_test.go @@ -0,0 +1,118 @@ +package text + +import ( + "testing" +) + +var ( + testTF1 Typeface = "MockFace" + testTF2 Typeface = "TestFace" + testTF3 Typeface = "AnotherFace" +) + +func TestClosestFontByWeight(t *testing.T) { + c := newTestCache( + Font{Style: Regular, Weight: Normal}, + Font{Style: Regular, Weight: Light}, + Font{Style: Regular, Weight: Bold}, + Font{Style: Italic, Weight: Thin}, + ) + weightOnlyTests := []struct { + Lookup Weight + Expected Weight + }{ + // Test for existing weights. + {Lookup: Normal, Expected: Normal}, + {Lookup: Light, Expected: Light}, + {Lookup: Bold, Expected: Bold}, + // Test for missing weights. + {Lookup: Thin, Expected: Light}, + {Lookup: ExtraLight, Expected: Light}, + {Lookup: Medium, Expected: Normal}, + {Lookup: SemiBold, Expected: Bold}, + {Lookup: ExtraBlack, Expected: Bold}, + } + for _, test := range weightOnlyTests { + got, ok := c.closestFont(Font{Typeface: testTF1, Weight: test.Lookup}) + if !ok { + t.Fatalf("expected closest font for %v to exist", test.Lookup) + } + if got.Weight != test.Expected { + t.Fatalf("got weight %v, expected %v", got.Weight, test.Expected) + } + } + c = newTestCache( + Font{Style: Regular, Weight: Light}, + Font{Style: Regular, Weight: Bold}, + Font{Style: Italic, Weight: Normal}, + Font{Typeface: testTF3, Style: Italic, Weight: Bold}, + ) + otherTests := []struct { + Lookup Font + Expected Font + ExpectedToFail bool + }{ + // Test for existing fonts. + { + Lookup: Font{Typeface: testTF1, Weight: Light}, + Expected: Font{Typeface: testTF1, Style: Regular, Weight: Light}, + }, + { + Lookup: Font{Typeface: testTF1, Style: Italic, Weight: Normal}, + Expected: Font{Typeface: testTF1, Style: Italic, Weight: Normal}, + }, + // Test for missing fonts. + { + Lookup: Font{Typeface: testTF1, Weight: Normal}, + Expected: Font{Typeface: testTF1, Style: Regular, Weight: Light}, + }, + { + Lookup: Font{Typeface: testTF3, Style: Italic, Weight: Normal}, + Expected: Font{Typeface: testTF3, Style: Italic, Weight: Bold}, + }, + { + Lookup: Font{Typeface: testTF1, Style: Italic, Weight: Thin}, + Expected: Font{Typeface: testTF1, Style: Italic, Weight: Normal}, + }, + { + Lookup: Font{Typeface: testTF1, Style: Italic, Weight: Bold}, + Expected: Font{Typeface: testTF1, Style: Italic, Weight: Normal}, + }, + { + Lookup: Font{Typeface: testTF2, Weight: Normal}, + ExpectedToFail: true, + }, + { + Lookup: Font{Typeface: testTF2, Style: Italic, Weight: Normal}, + ExpectedToFail: true, + }, + } + for _, test := range otherTests { + got, ok := c.closestFont(test.Lookup) + if test.ExpectedToFail { + if ok { + t.Fatalf("expected closest font for %v to not exist", test.Lookup) + } else { + continue + } + } + if !ok { + t.Fatalf("expected closest font for %v to exist", test.Lookup) + } + if got != test.Expected { + t.Fatalf("got %v, expected %v", got, test.Expected) + } + } +} + +func newTestCache(fonts ...Font) *Cache { + c := &Cache{faces: make(map[Font]*faceCache)} + c.def = testTF1 + for _, font := range fonts { + if font.Typeface == "" { + font.Typeface = testTF1 + } + c.faces[font] = &faceCache{face: nil} + } + return c +} diff --git a/text/text.go b/text/text.go index 2a375b44..d4188c45 100644 --- a/text/text.go +++ b/text/text.go @@ -114,3 +114,15 @@ func (s Style) String() string { panic("invalid Style") } } + +// weightDistance returns the distance value between two font weights. +func weightDistance(wa Weight, wb Weight) int { + // Avoid dealing with negative Weight values. + a := int(wa) + 400 + b := int(wb) + 400 + diff := a - b + if diff < 0 { + return -diff + } + return diff +}