gg icon indicating copy to clipboard operation
gg copied to clipboard

MeasureString underestimates size of string

Open dmjones opened this issue 4 years ago • 3 comments

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 avatar Jul 17 '21 17:07 dmjones

The issue is more significant when there are descenders:

image

dmjones avatar Jul 18 '21 07:07 dmjones

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.

dmjones avatar Jul 19 '21 20:07 dmjones

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
}

AltSpaceX avatar Jul 26 '22 12:07 AltSpaceX