fogleman / gg

Go Graphics - 2D rendering in Go with a simple API.
https://godoc.org/github.com/fogleman/gg
MIT License
4.37k stars 354 forks source link

MeasureString underestimates size of string #137

Open dmjones opened 3 years ago

dmjones commented 3 years ago

I've noticed MeasureString slightly underestimates the size of a string. Here is the code that I used to test this:

dc := gg.NewContext(300, 100)
dc.SetColor(color.White)
dc.DrawRectangle(0, 0, float64(dc.Width()), float64(dc.Height()))
dc.Fill()

fontPath := filepath.Join("fonts", "Merriweather-Regular.ttf")
if err := dc.LoadFontFace(fontPath, 42); err != nil {
    panic(err)
}

const s = "Hello, World!"
const offset = 10.0
w, h := dc.MeasureString(s)

// Draw rectangle at apparent text size
dc.SetRGB(1, 0, 0)
dc.DrawRectangle(offset, offset, w, h)
dc.Fill()

// Draw text
dc.SetRGB(0, 1, 0)
dc.DrawString(s, offset, offset+h)

if err := dc.SavePNG("out.png"); err != nil {
    panic(err)
}

The net result is this image:

image

Note how small aspects of the text go outside the bounding box. I'm far from a font expert, so perhaps this is to be expected.

dmjones commented 3 years ago

The issue is more significant when there are descenders:

image

dmjones commented 3 years ago

I notice the line height is set differently depending if one calls LoadFontFace or SetFontFace. If we change LoadFontFace to look like this:

func (dc *Context) LoadFontFace(path string, points float64) error {
    face, err := LoadFontFace(path, points)
    if err == nil {
        dc.SetFontFace(face)
    }
    return err
}

Then at least the red box becomes the correct size for the text. By estimating the offset (in this case, 8), you can then draw a perfect bounding box:

        // Modify this line (from opening example)
    dc.DrawString(s, offset, offset+h-8)

image

The magic value of 8 could be calculated by inspecting the font metrics, but unfortunately the Height value does not get set correctly (see https://github.com/golang/freetype/issues/59). We ought to be able to do Height - Ascent to calculate the value, but alas not.

Not really sure if there is a solution here.

AltSpaceX commented 2 years ago

I worked around this in a slightly clunky way for where I really need to know the height of a string, and using the font Size is too coarse.

func getStringHeight(s string, style TextStyleType) float64 {

    ctx := gg.NewContext(int(style.Size), int(style.Size)*len(s))

    ctx.SetFontFace(style.getFace())
    ctx.SetColor(style.getColour())
    ctx.DrawString(s, 0, style.Size)
    stringImage := ctx.Image()

    var stringHeight float64

    for y := 0; y < stringImage.Bounds().Dy(); y++ {
        for x := 0; x < stringImage.Bounds().Dx(); x++ {
            if _, _, _, alpha := stringImage.At(x, y).RGBA(); alpha > 0 {
                stringHeight = style.Size - float64(y)
                break
            }
        }
        if stringHeight > 0 {
            break
        }
    }

    return stringHeight
}

Haven't tidied it up, so there are references to my own functions in there, but you get the idea. It's a bit of a kludge, but it draws the string in a context, and then scans down the image to find the first pixel that's been drawn.

I had to do something similar for dealing with font descenders:

func getLengthOfStringDescender(style TextStyleType) float64 {

    var checkString string = "ygpqf"

    ctx := gg.NewContext(int(style.Size)*2, int(style.Size)*len(checkString))
    ctx.SetFontFace(style.getFace())
    ctx.SetColor(style.getColour())
    ctx.DrawString(checkString, 0, style.Size)
    stringImage := ctx.Image()

    var descenderLength float64

    for y := stringImage.Bounds().Dy(); y > 0; y-- {
        for x := stringImage.Bounds().Dx(); x > 0; x-- {
            if _, _, _, alpha := stringImage.At(x, y).RGBA(); alpha > 0 {
                descenderLength = float64(y) - style.Size
                break
            }
        }
        if descenderLength > 0 {
            break
        }
    }
    return descenderLength
}