text: improve rendering quality with HintingVertical

When HintingVertical is used, the interval between two glyphs is not
quantized (i.e. not a whole pixel). The text package didn't consider
this situation.

This change improves the quality by using more various glyph images
with 1/4 pixels granularity in vertical direction.

Closes #2469
This commit is contained in:
Hajime Hoshi 2022-11-23 18:13:54 +09:00
parent 56788cf8d9
commit a042af98b1
6 changed files with 41 additions and 18 deletions

View File

@ -46,7 +46,7 @@ func init() {
mplusSmallFont, err = opentype.NewFace(tt, &opentype.FaceOptions{ mplusSmallFont, err = opentype.NewFace(tt, &opentype.FaceOptions{
Size: 24, Size: 24,
DPI: dpi, DPI: dpi,
Hinting: font.HintingFull, Hinting: font.HintingVertical,
}) })
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@ -54,7 +54,7 @@ func init() {
mplusNormalFont, err = opentype.NewFace(tt, &opentype.FaceOptions{ mplusNormalFont, err = opentype.NewFace(tt, &opentype.FaceOptions{
Size: 32, Size: 32,
DPI: dpi, DPI: dpi,
Hinting: font.HintingFull, Hinting: font.HintingVertical,
}) })
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@ -62,7 +62,7 @@ func init() {
mplusBigFont, err = opentype.NewFace(tt, &opentype.FaceOptions{ mplusBigFont, err = opentype.NewFace(tt, &opentype.FaceOptions{
Size: 48, Size: 48,
DPI: dpi, DPI: dpi,
Hinting: font.HintingFull, Hinting: font.HintingVertical,
}) })
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)

View File

@ -96,7 +96,7 @@ func init() {
mplusNormalFont, err = opentype.NewFace(tt, &opentype.FaceOptions{ mplusNormalFont, err = opentype.NewFace(tt, &opentype.FaceOptions{
Size: 24, Size: 24,
DPI: dpi, DPI: dpi,
Hinting: font.HintingFull, Hinting: font.HintingVertical,
}) })
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@ -104,7 +104,7 @@ func init() {
mplusBigFont, err = opentype.NewFace(tt, &opentype.FaceOptions{ mplusBigFont, err = opentype.NewFace(tt, &opentype.FaceOptions{
Size: 48, Size: 48,
DPI: dpi, DPI: dpi,
Hinting: font.HintingFull, Hinting: font.HintingFull, // Use quantization to save glyph cache images.
}) })
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)

View File

@ -58,7 +58,7 @@ func initFont() {
mplusFont, err = opentype.NewFace(tt, &opentype.FaceOptions{ mplusFont, err = opentype.NewFace(tt, &opentype.FaceOptions{
Size: 12 * ebiten.DeviceScaleFactor(), Size: 12 * ebiten.DeviceScaleFactor(),
DPI: dpi, DPI: dpi,
Hinting: font.HintingFull, Hinting: font.HintingVertical,
}) })
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)

View File

@ -53,7 +53,7 @@ func init() {
mplusNormalFont, err = opentype.NewFace(tt, &opentype.FaceOptions{ mplusNormalFont, err = opentype.NewFace(tt, &opentype.FaceOptions{
Size: 24, Size: 24,
DPI: dpi, DPI: dpi,
Hinting: font.HintingFull, Hinting: font.HintingVertical,
}) })
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@ -61,7 +61,7 @@ func init() {
mplusBigFont, err = opentype.NewFace(tt, &opentype.FaceOptions{ mplusBigFont, err = opentype.NewFace(tt, &opentype.FaceOptions{
Size: 32, Size: 32,
DPI: dpi, DPI: dpi,
Hinting: font.HintingFull, Hinting: font.HintingVertical,
}) })
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)

View File

@ -57,7 +57,7 @@ func init() {
uiFont, err = opentype.NewFace(tt, &opentype.FaceOptions{ uiFont, err = opentype.NewFace(tt, &opentype.FaceOptions{
Size: 12, Size: 12,
DPI: 72, DPI: 72,
Hinting: font.HintingFull, Hinting: font.HintingVertical,
}) })
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)

View File

@ -48,6 +48,10 @@ func fixed26_6ToFloat64(x fixed.Int26_6) float64 {
return float64(x>>6) + float64(x&((1<<6)-1))/float64(1<<6) return float64(x>>6) + float64(x&((1<<6)-1))/float64(1<<6)
} }
func adjustOffsetGranularity(x fixed.Int26_6) fixed.Int26_6 {
return x / (1 << 4) * (1 << 4)
}
func drawGlyph(dst *ebiten.Image, face font.Face, r rune, img *ebiten.Image, topleft fixed.Point26_6, op *ebiten.DrawImageOptions) { func drawGlyph(dst *ebiten.Image, face font.Face, r rune, img *ebiten.Image, topleft fixed.Point26_6, op *ebiten.DrawImageOptions) {
if img == nil { if img == nil {
return return
@ -83,21 +87,30 @@ func getGlyphBounds(face font.Face, r rune) fixed.Rectangle26_6 {
return b return b
} }
type glyphImageCacheKey struct {
rune rune
xoffset fixed.Int26_6
}
type glyphImageCacheEntry struct { type glyphImageCacheEntry struct {
image *ebiten.Image image *ebiten.Image
atime int64 atime int64
} }
var ( var (
glyphImageCache = map[font.Face]map[rune]*glyphImageCacheEntry{} glyphImageCache = map[font.Face]map[glyphImageCacheKey]*glyphImageCacheEntry{}
) )
func getGlyphImage(face font.Face, r rune, offset fixed.Point26_6) *ebiten.Image { func getGlyphImage(face font.Face, r rune, offset fixed.Point26_6) *ebiten.Image {
if _, ok := glyphImageCache[face]; !ok { if _, ok := glyphImageCache[face]; !ok {
glyphImageCache[face] = map[rune]*glyphImageCacheEntry{} glyphImageCache[face] = map[glyphImageCacheKey]*glyphImageCacheEntry{}
} }
if e, ok := glyphImageCache[face][r]; ok { key := glyphImageCacheKey{
rune: r,
xoffset: offset.X,
}
if e, ok := glyphImageCache[face][key]; ok {
e.atime = now() e.atime = now()
return e.image return e.image
} }
@ -105,7 +118,7 @@ func getGlyphImage(face font.Face, r rune, offset fixed.Point26_6) *ebiten.Image
b := getGlyphBounds(face, r) b := getGlyphBounds(face, r)
w, h := (b.Max.X - b.Min.X).Ceil(), (b.Max.Y - b.Min.Y).Ceil() w, h := (b.Max.X - b.Min.X).Ceil(), (b.Max.Y - b.Min.Y).Ceil()
if w == 0 || h == 0 { if w == 0 || h == 0 {
glyphImageCache[face][r] = &glyphImageCacheEntry{ glyphImageCache[face][key] = &glyphImageCacheEntry{
image: nil, image: nil,
atime: now(), atime: now(),
} }
@ -133,7 +146,7 @@ func getGlyphImage(face font.Face, r rune, offset fixed.Point26_6) *ebiten.Image
d.DrawString(string(r)) d.DrawString(string(r))
img := ebiten.NewImageFromImage(rgba) img := ebiten.NewImageFromImage(rgba)
glyphImageCache[face][r] = &glyphImageCacheEntry{ glyphImageCache[face][key] = &glyphImageCacheEntry{
image: img, image: img,
atime: now(), atime: now(),
} }
@ -242,7 +255,7 @@ func DrawWithOptions(dst *ebiten.Image, text string, face font.Face, options *eb
// The current glyph images assume that they are rendered on integer positions so far. // The current glyph images assume that they are rendered on integer positions so far.
b := getGlyphBounds(face, r) b := getGlyphBounds(face, r)
offset := fixed.Point26_6{ offset := fixed.Point26_6{
X: b.Min.X & ((1 << 6) - 1), X: (adjustOffsetGranularity(dx) + b.Min.X) & ((1 << 6) - 1),
Y: b.Min.Y & ((1 << 6) - 1), Y: b.Min.Y & ((1 << 6) - 1),
} }
img := getGlyphImage(face, r, offset) img := getGlyphImage(face, r, offset)
@ -353,13 +366,24 @@ func CacheGlyphs(face font.Face, text string) {
textM.Lock() textM.Lock()
defer textM.Unlock() defer textM.Unlock()
var dx fixed.Int26_6
prevR := rune(-1)
for _, r := range text { for _, r := range text {
if prevR >= 0 {
dx += face.Kern(prevR, r)
}
if r == '\n' {
dx = 0
continue
}
b := getGlyphBounds(face, r) b := getGlyphBounds(face, r)
offset := fixed.Point26_6{ offset := fixed.Point26_6{
X: b.Min.X & ((1 << 6) - 1), X: (adjustOffsetGranularity(dx) + b.Min.X) & ((1 << 6) - 1),
Y: b.Min.Y & ((1 << 6) - 1), Y: b.Min.Y & ((1 << 6) - 1),
} }
getGlyphImage(face, r, offset) getGlyphImage(face, r, offset)
dx += glyphAdvance(face, r)
prevR = r
} }
} }
@ -448,7 +472,7 @@ func AppendGlyphs(glyphs []Glyph, face font.Face, text string) []Glyph {
b := getGlyphBounds(face, r) b := getGlyphBounds(face, r)
offset := fixed.Point26_6{ offset := fixed.Point26_6{
X: b.Min.X & ((1 << 6) - 1), X: (adjustOffsetGranularity(pos.X) + b.Min.X) & ((1 << 6) - 1),
Y: b.Min.Y & ((1 << 6) - 1), Y: b.Min.Y & ((1 << 6) - 1),
} }
if img := getGlyphImage(face, r, offset); img != nil { if img := getGlyphImage(face, r, offset); img != nil {
@ -463,7 +487,6 @@ func AppendGlyphs(glyphs []Glyph, face font.Face, text string) []Glyph {
}) })
} }
pos.X += glyphAdvance(face, r) pos.X += glyphAdvance(face, r)
prevR = r prevR = r
} }