freetype/truetype: calculate advance widths correctly.

The logic is a little clunky, but as before, let's get it right first,
then get it clean once we have a full battery of tests.

R=bsiegert
CC=golang-codereviews
https://codereview.appspot.com/50910043
This commit is contained in:
Nigel Tao 2014-01-14 09:58:07 +11:00
parent 897255e610
commit 10cb3b4280
4 changed files with 108 additions and 75 deletions

View file

@ -31,10 +31,11 @@ const (
// implicitly by the quantized x and y fractional offset. It maps to a mask
// image and an offset.
type cacheEntry struct {
valid bool
glyph truetype.Index
mask *image.Alpha
offset image.Point
valid bool
glyph truetype.Index
advanceWidth raster.Fix32
mask *image.Alpha
offset image.Point
}
// ParseFont just calls the Parse function from the freetype/truetype package.
@ -124,12 +125,14 @@ func (c *Context) drawContour(ps []truetype.Point, dx, dy raster.Fix32) {
}
}
// rasterize returns the glyph mask and integer-pixel offset to render the
// given glyph at the given sub-pixel offsets.
// rasterize returns the advance width, glyph mask and integer-pixel offset
// to render the given glyph at the given sub-pixel offsets.
// The 24.8 fixed point arguments fx and fy must be in the range [0, 1).
func (c *Context) rasterize(glyph truetype.Index, fx, fy raster.Fix32) (*image.Alpha, image.Point, error) {
func (c *Context) rasterize(glyph truetype.Index, fx, fy raster.Fix32) (
raster.Fix32, *image.Alpha, image.Point, error) {
if err := c.glyphBuf.Load(c.font, c.scale, glyph, nil); err != nil {
return nil, image.ZP, err
return 0, nil, image.Point{}, err
}
// Calculate the integer-pixel bounds for the glyph.
xmin := int(fx+raster.Fix32(c.glyphBuf.B.XMin<<2)) >> 8
@ -137,7 +140,7 @@ func (c *Context) rasterize(glyph truetype.Index, fx, fy raster.Fix32) (*image.A
xmax := int(fx+raster.Fix32(c.glyphBuf.B.XMax<<2)+0xff) >> 8
ymax := int(fy-raster.Fix32(c.glyphBuf.B.YMin<<2)+0xff) >> 8
if xmin > xmax || ymin > ymax {
return nil, image.ZP, errors.New("freetype: negative sized glyph")
return 0, nil, image.Point{}, errors.New("freetype: negative sized glyph")
}
// A TrueType's glyph's nodes can have negative co-ordinates, but the
// rasterizer clips anything left of x=0 or above y=0. xmin and ymin
@ -155,13 +158,16 @@ func (c *Context) rasterize(glyph truetype.Index, fx, fy raster.Fix32) (*image.A
}
a := image.NewAlpha(image.Rect(0, 0, xmax-xmin, ymax-ymin))
c.r.Rasterize(raster.NewAlphaSrcPainter(a))
return a, image.Point{xmin, ymin}, nil
return raster.Fix32(c.glyphBuf.AdvanceWidth << 2), a, image.Point{xmin, ymin}, nil
}
// glyph returns the glyph mask and integer-pixel offset to render the given
// glyph at the given sub-pixel point. It is a cache for the rasterize method.
// Unlike rasterize, p's co-ordinates do not have to be in the range [0, 1).
func (c *Context) glyph(glyph truetype.Index, p raster.Point) (*image.Alpha, image.Point, error) {
// glyph returns the advance width, glyph mask and integer-pixel offset to
// render the given glyph at the given sub-pixel point. It is a cache for the
// rasterize method. Unlike rasterize, p's co-ordinates do not have to be in
// the range [0, 1).
func (c *Context) glyph(glyph truetype.Index, p raster.Point) (
raster.Fix32, *image.Alpha, image.Point, error) {
// Split p.X and p.Y into their integer and fractional parts.
ix, fx := int(p.X>>8), p.X&0xff
iy, fy := int(p.Y>>8), p.Y&0xff
@ -171,16 +177,16 @@ func (c *Context) glyph(glyph truetype.Index, p raster.Point) (*image.Alpha, ima
ty := int(fy) / (256 / nYFractions)
t := ((tg*nXFractions)+tx)*nYFractions + ty
// Check for a cache hit.
if c.cache[t].valid && c.cache[t].glyph == glyph {
return c.cache[t].mask, c.cache[t].offset.Add(image.Point{ix, iy}), nil
if e := c.cache[t]; e.valid && e.glyph == glyph {
return e.advanceWidth, e.mask, e.offset.Add(image.Point{ix, iy}), nil
}
// Rasterize the glyph and put the result into the cache.
mask, offset, err := c.rasterize(glyph, fx, fy)
advanceWidth, mask, offset, err := c.rasterize(glyph, fx, fy)
if err != nil {
return nil, image.ZP, err
return 0, nil, image.Point{}, err
}
c.cache[t] = cacheEntry{true, glyph, mask, offset}
return mask, offset.Add(image.Point{ix, iy}), nil
c.cache[t] = cacheEntry{true, glyph, advanceWidth, mask, offset}
return advanceWidth, mask, offset.Add(image.Point{ix, iy}), nil
}
// DrawString draws s at p and returns p advanced by the text extent. The text
@ -198,13 +204,14 @@ func (c *Context) DrawString(s string, p raster.Point) (raster.Point, error) {
for _, rune := range s {
index := c.font.Index(rune)
if hasPrev {
// TODO: adjust for hinting.
p.X += raster.Fix32(c.font.Kerning(c.scale, prev, index)) << 2
}
mask, offset, err := c.glyph(index, p)
advanceWidth, mask, offset, err := c.glyph(index, p)
if err != nil {
return raster.Point{}, err
}
p.X += raster.Fix32(c.font.HMetric(c.scale, index).AdvanceWidth) << 2
p.X += advanceWidth
glyphRect := mask.Bounds().Add(offset)
dr := c.clip.Intersect(glyphRect)
if !dr.Empty() {

View file

@ -17,6 +17,8 @@ type Point struct {
// A GlyphBuf holds a glyph's contours. A GlyphBuf can be re-used to load a
// series of glyphs from a Font.
type GlyphBuf struct {
// AdvanceWidth is the glyph's advance width.
AdvanceWidth int32
// B is the glyph's bounding box.
B Bounds
// Point contains all Points from all contours of the glyph. If a
@ -106,6 +108,22 @@ func (g *GlyphBuf) Load(f *Font, scale int32, i Index, h *Hinter) error {
}
}
advanceWidth := g.phantomPoints[1].X - g.phantomPoints[0].X
if h != nil {
if len(f.hdmx) >= 8 {
if n := u32(f.hdmx, 4); n > 3+uint32(i) {
for hdmx := f.hdmx[8:]; uint32(len(hdmx)) >= n; hdmx = hdmx[n:] {
if int32(hdmx[0]) == scale>>6 {
advanceWidth = int32(hdmx[2+i]) << 6
break
}
}
}
}
advanceWidth = (advanceWidth + 32) &^ 63
}
g.AdvanceWidth = advanceWidth
// Set g.B to the 'control box', which is the bounding box of the Bézier
// curves' control points. This is easier to calculate, no smaller than
// and often equal to the tightest possible bounding box of the curves
@ -159,16 +177,17 @@ func (g *GlyphBuf) load(recursion int32, i Index, useMyMetrics bool) (err error)
g0 = u32(g.font.loca, 4*int(i))
g1 = u32(g.font.loca, 4*int(i)+4)
}
if g0 == g1 {
return nil
}
glyf := g.font.glyf[g0:g1]
// Decode the contour count and nominal bounding box.
// boundsYMin and boundsXMax, at offsets 4 and 6, are unused.
ne := int(int16(u16(glyf, 0)))
boundsXMin := int32(int16(u16(glyf, 2)))
boundsYMax := int32(int16(u16(glyf, 8)))
// Decode the contour count and nominal bounding box, from the first
// 10 bytes of the glyf data. boundsYMin and boundsXMax, at offsets 4
// and 6, are unused.
glyf, ne, boundsXMin, boundsYMax := []byte(nil), 0, int32(0), int32(0)
if g0+10 <= g1 {
glyf = g.font.glyf[g0:g1]
ne = int(int16(u16(glyf, 0)))
boundsXMin = int32(int16(u16(glyf, 2)))
boundsYMax = int32(int16(u16(glyf, 8)))
}
// Create the phantom points.
uhm, pp1x := g.font.unscaledHMetric(i), int32(0)
@ -179,6 +198,12 @@ func (g *GlyphBuf) load(recursion int32, i Index, useMyMetrics bool) (err error)
{X: uhm.AdvanceWidth / 2, Y: boundsYMax + uvm.TopSideBearing},
{X: uhm.AdvanceWidth / 2, Y: boundsYMax + uvm.TopSideBearing - uvm.AdvanceHeight},
}
if len(glyf) == 0 {
g.addPhantomsAndScale(len(g.Point), len(g.Point), true, true)
copy(g.phantomPoints[:], g.Point[len(g.Point)-4:])
g.Point = g.Point[:len(g.Point)-4]
return nil
}
// Load and hint the contours.
if ne < 0 {
@ -194,7 +219,7 @@ func (g *GlyphBuf) load(recursion int32, i Index, useMyMetrics bool) (err error)
} else {
np0, ne0 := len(g.Point), len(g.End)
program := g.loadSimple(glyf, ne)
g.addPhantomsAndScale(np0, np0, true)
g.addPhantomsAndScale(np0, np0, true, true)
pp1x = g.Point[len(g.Point)-4].X
if g.hinter != nil {
if len(program) != 0 {
@ -213,7 +238,9 @@ func (g *GlyphBuf) load(recursion int32, i Index, useMyMetrics bool) (err error)
g.InFontUnits = g.InFontUnits[:len(g.InFontUnits)-4]
g.Unhinted = g.Unhinted[:len(g.Unhinted)-4]
}
copy(g.phantomPoints[:], g.Point[len(g.Point)-4:])
if useMyMetrics {
copy(g.phantomPoints[:], g.Point[len(g.Point)-4:])
}
g.Point = g.Point[:len(g.Point)-4]
if np0 != 0 {
// The hinting program expects the []End values to be indexed relative
@ -397,22 +424,28 @@ func (g *GlyphBuf) loadCompound(recursion int32, uhm HMetric, i Index,
}
}
// Hint the compound glyph.
if g.hinter == nil || offset+2 > len(glyf) {
return nil
instrLen := 0
if g.hinter != nil && offset+2 <= len(glyf) {
instrLen = int(u16(glyf, offset))
offset += 2
}
instrLen := int(u16(glyf, offset))
offset += 2
if instrLen == 0 {
return nil
}
program := glyf[offset : offset+instrLen]
g.addPhantomsAndScale(np0, len(g.Point), false)
g.addPhantomsAndScale(np0, len(g.Point), false, instrLen > 0)
points, ends := g.Point[np0:], g.End[ne0:]
g.Point = g.Point[:len(g.Point)-4]
for j := range points {
points[j].Flags &^= flagTouchedX | flagTouchedY
}
if instrLen == 0 {
if !g.metricsSet {
copy(g.phantomPoints[:], points[len(points)-4:])
}
return nil
}
// Hint the compound glyph.
program := glyf[offset : offset+instrLen]
// Temporarily adjust the ends to be relative to this compound glyph.
if np0 != 0 {
for i := range ends {
@ -430,11 +463,13 @@ func (g *GlyphBuf) loadCompound(recursion int32, uhm HMetric, i Index,
ends[i] += np0
}
}
copy(g.phantomPoints[:], points[len(points)-4:])
if !g.metricsSet {
copy(g.phantomPoints[:], points[len(points)-4:])
}
return nil
}
func (g *GlyphBuf) addPhantomsAndScale(np0, np1 int, simple bool) {
func (g *GlyphBuf) addPhantomsAndScale(np0, np1 int, simple, adjust bool) {
// Add the four phantom points.
g.Point = append(g.Point, g.phantomPoints[:]...)
// Scale the points.
@ -446,19 +481,24 @@ func (g *GlyphBuf) addPhantomsAndScale(np0, np1 int, simple bool) {
p.X = g.font.scale(g.scale * p.X)
p.Y = g.font.scale(g.scale * p.Y)
}
if g.hinter != nil {
// Round the 1st phantom point to the grid, shifting all other points equally.
// Note that "all other points" starts from np0, not np1.
// TODO: is the np0/np1 distinction actually a bug in C Freetype?
if g.hinter == nil {
return
}
// Round the 1st phantom point to the grid, shifting all other points equally.
// Note that "all other points" starts from np0, not np1.
// TODO: delete this adjustment and the np0/np1 distinction, when
// we update the compatibility tests to C Freetype 2.5.3.
// See http://git.savannah.gnu.org/cgit/freetype/freetype2.git/commit/?id=05c786d990390a7ca18e62962641dac740bacb06
if adjust {
pp1x := g.Point[len(g.Point)-4].X
if dx := ((pp1x + 32) &^ 63) - pp1x; dx != 0 {
for i := np0; i < len(g.Point); i++ {
g.Point[i].X += dx
}
}
if simple {
g.Unhinted = append(g.Unhinted, g.Point[np1:]...)
}
}
if simple {
g.Unhinted = append(g.Unhinted, g.Point[np1:]...)
}
// Round the 2nd and 4th phantom point to the grid.
p := &g.Point[len(g.Point)-3]

View file

@ -98,7 +98,7 @@ type cm struct {
type Font struct {
// Tables sliced from the TTF data. The different tables are documented
// at http://developer.apple.com/fonts/TTRefMan/RM06/Chap6.html
cmap, cvt, fpgm, glyf, head, hhea, hmtx, kern, loca, maxp, os2, prep, vmtx []byte
cmap, cvt, fpgm, glyf, hdmx, head, hhea, hmtx, kern, loca, maxp, os2, prep, vmtx []byte
cmapIndexes []byte
@ -502,6 +502,8 @@ func parse(ttf []byte, offset int) (font *Font, err error) {
f.fpgm, err = readTable(ttf, ttf[x+8:x+16])
case "glyf":
f.glyf, err = readTable(ttf, ttf[x+8:x+16])
case "hdmx":
f.hdmx, err = readTable(ttf, ttf[x+8:x+16])
case "head":
f.head, err = readTable(ttf, ttf[x+8:x+16])
case "hhea":

View file

@ -254,18 +254,12 @@ func scalingTestEquals(a, b []Point) (index int, equals bool) {
var scalingTestCases = []struct {
name string
size int32
// xxxHintingBrokenAt, if non-negative, is the glyph index n for which
// only the first n glyphs are known to be correct wrt the advance width
// and bounding boxes.
// TODO: remove these fields.
sansHintingBrokenAt int
withHintingBrokenAt int
}{
{"luxisr", 12, -1, -1},
{"x-arial-bold", 11, -1, -1},
{"x-deja-vu-sans-oblique", 17, -1, -1},
{"x-droid-sans-japanese", 9, -1, -1},
{"x-times-new-roman", 13, -1, -1},
{"luxisr", 12},
{"x-arial-bold", 11},
{"x-deja-vu-sans-oblique", 17},
{"x-droid-sans-japanese", 9},
{"x-times-new-roman", 13},
}
func testScaling(t *testing.T, hinter *Hinter) {
@ -320,31 +314,21 @@ func testScaling(t *testing.T, hinter *Hinter) {
glyphBuf := NewGlyphBuf()
for i, want := range wants {
if hinter == nil && i == tc.sansHintingBrokenAt {
break
}
if hinter != nil && i == tc.withHintingBrokenAt {
break
}
if err = glyphBuf.Load(font, tc.size*64, Index(i), hinter); err != nil {
t.Errorf("%s: glyph #%d: Load: %v", tc.name, i, err)
continue
}
got := scalingTestData{
// TODO: a GlyphBuf should also provide the advance width.
advanceWidth: font.HMetric(tc.size*64, Index(i)).AdvanceWidth,
advanceWidth: glyphBuf.AdvanceWidth,
bounds: glyphBuf.B,
points: glyphBuf.Point,
}
/* TODO: check advance widths.
if got.advanceWidth != want.advanceWidth {
t.Errorf("%s: glyph #%d advance width:\ngot %v\nwant %v",
tc.name, i, got.advanceWidth, want.advanceWidth)
continue
}
*/
if got.bounds != want.bounds {
t.Errorf("%s: glyph #%d bounds:\ngot %v\nwant %v",