From a3c53fdc3f57cc30d94076687be297892efd5d24 Mon Sep 17 00:00:00 2001 From: Nigel Tao Date: Sat, 31 Aug 2013 16:08:40 +1000 Subject: [PATCH] freetype/truetype: implement ALIGNRP, MDAP and MDRP opcodes. We can now hint the .notdef glyph from luxisr.ttf. Yay. R=bsiegert CC=golang-dev https://codereview.appspot.com/12829048 --- freetype/truetype/glyph.go | 72 +++++++++--- freetype/truetype/hint.go | 170 +++++++++++++++++++++++++++-- freetype/truetype/hint_test.go | 3 +- freetype/truetype/opcodes.go | 82 +++++++------- freetype/truetype/truetype_test.go | 15 ++- 5 files changed, 272 insertions(+), 70 deletions(-) diff --git a/freetype/truetype/glyph.go b/freetype/truetype/glyph.go index 2f47e43..52ebaa7 100644 --- a/freetype/truetype/glyph.go +++ b/freetype/truetype/glyph.go @@ -19,8 +19,13 @@ type Point struct { type GlyphBuf struct { // The glyph's bounding box. B Bounds - // Point contains all Points from all contours of the glyph. - Point []Point + // Point contains all Points from all contours of the glyph. If a + // Hinter was used to load a glyph then Unhinted contains those + // Points before they were hinted, and InFontUnits contains those + // Points before they were hinted and scaled. Twilight is those + // Points created in the 'twilight zone' by the truetype hinting + // process. + Point, Unhinted, InFontUnits, Twilight []Point // The length of End is the number of contours in the glyph. The i'th // contour consists of points Point[End[i-1]:End[i]], where End[-1] // is interpreted to mean zero. @@ -36,6 +41,10 @@ const ( flagRepeat flagPositiveXShortVector flagPositiveYShortVector + + // The remaining flags are for internal use. + flagTouchedX + flagTouchedY ) // The same flag bits (0x10 and 0x20) are overloaded to have two meanings, @@ -112,25 +121,27 @@ func (g *GlyphBuf) Load(f *Font, scale int32, i Index, h *Hinter) error { // Reset the GlyphBuf. g.B = Bounds{} g.Point = g.Point[:0] + g.Unhinted = g.Unhinted[:0] + g.InFontUnits = g.InFontUnits[:0] + g.Twilight = g.Twilight[:0] g.End = g.End[:0] - if err := g.load(f, scale, i, 0, 0, false, 0); err != nil { + if h != nil { + if err := h.init(g, f, scale); err != nil { + return err + } + } + if err := g.load(f, scale, i, h, 0, 0, false, 0); err != nil { return err } g.B.XMin = f.scale(scale * g.B.XMin) g.B.YMin = f.scale(scale * g.B.YMin) g.B.XMax = f.scale(scale * g.B.XMax) g.B.YMax = f.scale(scale * g.B.YMax) - if h != nil { - if err := h.init(f, scale); err != nil { - return err - } - // TODO: invoke h. - } return nil } // loadCompound loads a glyph that is composed of other glyphs. -func (g *GlyphBuf) loadCompound(f *Font, scale int32, glyf []byte, offset int, +func (g *GlyphBuf) loadCompound(f *Font, scale int32, h *Hinter, glyf []byte, offset int, dx, dy int32, recursion int) error { // Flags for decoding a compound glyph. These flags are documented at @@ -150,7 +161,7 @@ func (g *GlyphBuf) loadCompound(f *Font, scale int32, glyf []byte, offset int, ) for { flags := u16(glyf, offset) - component := u16(glyf, offset+2) + component := Index(u16(glyf, offset+2)) dx1, dy1 := dx, dy if flags&flagArg1And2AreWords != 0 { dx1 += int32(int16(u16(glyf, offset+4))) @@ -168,7 +179,7 @@ func (g *GlyphBuf) loadCompound(f *Font, scale int32, glyf []byte, offset int, return UnsupportedError("compound glyph scale/transform") } b0 := g.B - g.load(f, scale, Index(component), dx1, dy1, flags&flagRoundXYToGrid != 0, recursion+1) + g.load(f, scale, component, h, dx1, dy1, flags&flagRoundXYToGrid != 0, recursion+1) if flags&flagUseMyMetrics == 0 { g.B = b0 } @@ -180,7 +191,7 @@ func (g *GlyphBuf) loadCompound(f *Font, scale int32, glyf []byte, offset int, } // load appends a glyph's contours to this GlyphBuf. -func (g *GlyphBuf) load(f *Font, scale int32, i Index, +func (g *GlyphBuf) load(f *Font, scale int32, i Index, h *Hinter, dx, dy int32, roundDxDy bool, recursion int) error { if recursion >= 4 { @@ -207,7 +218,7 @@ func (g *GlyphBuf) load(f *Font, scale int32, i Index, g.B.YMax = int32(int16(u16(glyf, 8))) offset := 10 if ne == -1 { - return g.loadCompound(f, scale, glyf, offset, dx, dy, recursion) + return g.loadCompound(f, scale, h, glyf, offset, dx, dy, recursion) } else if ne < 0 { // http://developer.apple.com/fonts/TTRefMan/RM06/Chap6glyf.html says that // "the values -2, -3, and so forth, are reserved for future use." @@ -224,18 +235,33 @@ func (g *GlyphBuf) load(f *Font, scale int32, i Index, g.End[i] = 1 + np0 + int(u16(glyf, offset)) offset += 2 } - // Skip the TrueType hinting instructions. + + // Note the TrueType hinting instructions. instrLen := int(u16(glyf, offset)) - offset += 2 + instrLen + offset += 2 + program := glyf[offset : offset+instrLen] + offset += instrLen + // Decode the points. np := int(g.End[ne-1]) if np <= cap(g.Point) { g.Point = g.Point[:np] } else { + p := g.Point g.Point = make([]Point, np, np*2) + copy(g.Point, p) } offset = g.decodeFlags(glyf, offset, np0) g.decodeCoords(glyf, offset, np0) + + // Delta-adjust, scale and hint. + if h != nil { + g.InFontUnits = append(g.InFontUnits, g.Point[np0:np]...) + for i := np0; i < np; i++ { + g.InFontUnits[i].X += dx + g.InFontUnits[i].Y += dy + } + } if roundDxDy { dx = (f.scale(scale*dx) + 32) &^ 63 dy = (f.scale(scale*dy) + 32) &^ 63 @@ -249,9 +275,23 @@ func (g *GlyphBuf) load(f *Font, scale int32, i Index, g.Point[i].Y = f.scale(scale * (g.Point[i].Y + dy)) } } + if h != nil { + g.Unhinted = append(g.Unhinted, g.Point[np0:np]...) + if err := h.run(program); err != nil { + return err + } + } + return nil } +func (g *GlyphBuf) points(zonePointer int32) []Point { + if zonePointer == 0 { + return g.Twilight + } + return g.Point +} + // NewGlyphBuf returns a newly allocated GlyphBuf. func NewGlyphBuf() *GlyphBuf { g := new(GlyphBuf) diff --git a/freetype/truetype/hint.go b/freetype/truetype/hint.go index 1eb7d46..42b5617 100644 --- a/freetype/truetype/hint.go +++ b/freetype/truetype/hint.go @@ -28,9 +28,11 @@ type Hinter struct { // functions is a map from function number to bytecode. functions map[int32][]byte - // font and scale are the font and scale last used for this Hinter. - // Changing the font will require running the new font's fpgm bytecode. - // Changing either will require running the font's prep bytecode. + // g, font and scale are the glyph buffer, font and scale last used for + // this Hinter. Changing the font will require running the new font's + // fpgm bytecode. Changing either will require running the font's prep + // bytecode. + g *GlyphBuf font *Font scale int32 @@ -75,7 +77,9 @@ var globalDefaultGS = graphicsState{ autoFlip: true, } -func (h *Hinter) init(f *Font, scale int32) error { +func (h *Hinter) init(g *GlyphBuf, f *Font, scale int32) error { + h.g = g + rescale := h.scale != scale if h.font != f { h.font, rescale = f, true @@ -158,20 +162,20 @@ func (h *Hinter) run(program []byte) error { case opSVTCA0: h.gs.pv = [2]f2dot14{0, 0x4000} h.gs.fv = [2]f2dot14{0, 0x4000} - // TODO: h.gs.dv = h.gs.pv ?? + h.gs.dv = [2]f2dot14{0, 0x4000} case opSVTCA1: h.gs.pv = [2]f2dot14{0x4000, 0} h.gs.fv = [2]f2dot14{0x4000, 0} - // TODO: h.gs.dv = h.gs.pv ?? + h.gs.dv = [2]f2dot14{0x4000, 0} case opSPVTCA0: h.gs.pv = [2]f2dot14{0, 0x4000} - // TODO: h.gs.dv = h.gs.pv ?? + h.gs.dv = [2]f2dot14{0, 0x4000} case opSPVTCA1: h.gs.pv = [2]f2dot14{0x4000, 0} - // TODO: h.gs.dv = h.gs.pv ?? + h.gs.dv = [2]f2dot14{0x4000, 0} case opSFVTCA0: h.gs.fv = [2]f2dot14{0, 0x4000} @@ -360,6 +364,45 @@ func (h *Hinter) run(program []byte) error { } program, pc = callStack[callStackTop].program, callStack[callStackTop].pc + case opMDAP0, opMDAP1: + points := h.g.points(h.gs.zp[0]) + top-- + i := int(h.stack[top]) + if i < 0 || len(points) <= i { + return errors.New("truetype: hinting: point out of range") + } + p := &points[i] + distance := f26dot6(0) + if opcode == opMDAP1 { + distance = dotProduct(f26dot6(p.X), f26dot6(p.Y), h.gs.pv) + // TODO: metrics compensation. + distance = h.round(distance) - distance + } + h.move(p, distance) + h.gs.rp[0] = int32(i) + h.gs.rp[1] = int32(i) + + case opALIGNRP: + if top < int(h.gs.loop) { + return errors.New("truetype: hinting: stack underflow") + } + i, points := int(h.gs.rp[0]), h.g.points(h.gs.zp[0]) + if i < 0 || len(points) <= i { + return errors.New("truetype: hinting: point out of range") + } + ref := &points[i] + points = h.g.points(h.gs.zp[1]) + for ; h.gs.loop != 0; h.gs.loop-- { + top-- + i = int(h.stack[top]) + if i < 0 || len(points) <= i { + return errors.New("truetype: hinting: point out of range") + } + p := &points[i] + h.move(p, -dotProduct(f26dot6(p.X-ref.X), f26dot6(p.Y-ref.Y), h.gs.pv)) + } + h.gs.loop = 1 + case opRTDG: h.gs.roundPeriod = 1 << 5 h.gs.roundPhase = 0 @@ -617,6 +660,80 @@ func (h *Hinter) run(program []byte) error { opcode += 0x80 goto push + case opMDRP00000, opMDRP00001, opMDRP00010, opMDRP00011, + opMDRP00100, opMDRP00101, opMDRP00110, opMDRP00111, + opMDRP01000, opMDRP01001, opMDRP01010, opMDRP01011, + opMDRP01100, opMDRP01101, opMDRP01110, opMDRP01111, + opMDRP10000, opMDRP10001, opMDRP10010, opMDRP10011, + opMDRP10100, opMDRP10101, opMDRP10110, opMDRP10111, + opMDRP11000, opMDRP11001, opMDRP11010, opMDRP11011, + opMDRP11100, opMDRP11101, opMDRP11110, opMDRP11111: + + i, points := int(h.gs.rp[0]), h.g.points(h.gs.zp[0]) + if i < 0 || len(points) <= i { + return errors.New("truetype: hinting: point out of range") + } + ref := &points[i] + top-- + i = int(h.stack[top]) + points = h.g.points(h.gs.zp[1]) + if i < 0 || len(points) <= i { + return errors.New("truetype: hinting: point out of range") + } + p := &points[i] + + origDist := f26dot6(0) + if h.gs.zp[0] == 0 && h.gs.zp[1] == 0 { + p0 := &h.g.Unhinted[i] + p1 := &h.g.Unhinted[h.gs.rp[0]] + origDist = dotProduct(f26dot6(p0.X-p1.X), f26dot6(p0.Y-p1.Y), h.gs.dv) + } else { + p0 := &h.g.InFontUnits[i] + p1 := &h.g.InFontUnits[h.gs.rp[0]] + origDist = dotProduct(f26dot6(p0.X-p1.X), f26dot6(p0.Y-p1.Y), h.gs.dv) + origDist = f26dot6(h.font.scale(h.scale * int32(origDist))) + } + + // Single-width cut-in test. + if x := (origDist - h.gs.singleWidth).abs(); x < h.gs.singleWidthCutIn { + if origDist >= 0 { + origDist = h.gs.singleWidthCutIn + } else { + origDist = -h.gs.singleWidthCutIn + } + } + + // Rounding bit. + // TODO: metrics compensation. + distance := origDist + if opcode&0x04 != 0 { + distance = h.round(origDist) + } + + // Minimum distance bit. + if opcode&0x08 != 0 { + if origDist >= 0 { + if distance < h.gs.minDist { + distance = h.gs.minDist + } + } else { + if distance > -h.gs.minDist { + distance = -h.gs.minDist + } + } + } + + // Set-RP0 bit. + if opcode&0x10 != 0 { + h.gs.rp[0] = int32(i) + } + h.gs.rp[1] = h.gs.rp[0] + h.gs.rp[2] = int32(i) + + // Move the point. + origDist = dotProduct(f26dot6(p.X-ref.X), f26dot6(p.Y-ref.Y), h.gs.pv) + h.move(p, distance-origDist) + default: return errors.New("truetype: hinting: unrecognized instruction") } @@ -698,6 +815,27 @@ func (h *Hinter) run(program []byte) error { return nil } +func (h *Hinter) move(p *Point, distance f26dot6) { + if h.gs.fv[0] == 0 { + p.Y += int32(distance) + p.Flags |= flagTouchedY + return + } + if h.gs.fv[1] == 0 { + p.X += int32(distance) + p.Flags |= flagTouchedX + return + } + fvx := int64(h.gs.fv[0]) + fvy := int64(h.gs.fv[1]) + pvx := int64(h.gs.pv[0]) + pvy := int64(h.gs.pv[1]) + fvDotPv := (fvx*pvx + fvy*pvy) >> 14 + p.X += int32(int64(distance) * fvx / fvDotPv) + p.Y += int32(int64(distance) * fvy / fvDotPv) + p.Flags |= flagTouchedX | flagTouchedY +} + // skipInstructionPayload increments pc by the extra data that follows a // variable length PUSHB or PUSHW instruction. func skipInstructionPayload(program []byte, pc int) (newPC int, ok bool) { @@ -730,6 +868,14 @@ type f2dot14 int16 // f26dot6 is a 26.6 fixed point number. type f26dot6 int32 +// abs returns abs(x) in 26.6 fixed point arithmetic. +func (x f26dot6) abs() f26dot6 { + if x < 0 { + return -x + } + return x +} + // div returns x/y in 26.6 fixed point arithmetic. func (x f26dot6) div(y f26dot6) f26dot6 { return f26dot6((int64(x) << 6) / int64(y)) @@ -740,6 +886,14 @@ func (x f26dot6) mul(y f26dot6) f26dot6 { return f26dot6(int64(x) * int64(y) >> 6) } +func dotProduct(x, y f26dot6, q [2]f2dot14) f26dot6 { + px := int64(x) + py := int64(y) + qx := int64(q[0]) + qy := int64(q[1]) + return f26dot6((px*qx + py*qy) >> 14) +} + // round rounds the given number. The rounding algorithm is described at // https://developer.apple.com/fonts/TTRefMan/RM02/Chap2.html#rounding func (h *Hinter) round(x f26dot6) f26dot6 { diff --git a/freetype/truetype/hint_test.go b/freetype/truetype/hint_test.go index 81182d2..657bc26 100644 --- a/freetype/truetype/hint_test.go +++ b/freetype/truetype/hint_test.go @@ -553,9 +553,10 @@ func TestBytecode(t *testing.T) { }, } + var g GlyphBuf for _, tc := range testCases { h := &Hinter{} - h.init(&Font{ + h.init(&g, &Font{ maxStorage: 32, maxStackElements: 100, }, 768) diff --git a/freetype/truetype/opcodes.go b/freetype/truetype/opcodes.go index bb300b3..bd5ada5 100644 --- a/freetype/truetype/opcodes.go +++ b/freetype/truetype/opcodes.go @@ -56,8 +56,8 @@ const ( opCALL = 0x2b // CALL function opFDEF = 0x2c // Function DEFinition opENDF = 0x2d // END Function definition - opMDAP0 = 0x2e - opMDAP1 = 0x2f + opMDAP0 = 0x2e // Move Direct Absolute Point + opMDAP1 = 0x2f // . opIUP0 = 0x30 opIUP1 = 0x31 opSHP0 = 0x32 @@ -70,7 +70,7 @@ const ( opIP = 0x39 opMSIRP0 = 0x3a opMSIRP1 = 0x3b - opALIGNRP = 0x3c + opALIGNRP = 0x3c // ALIGN to Reference Point opRTDG = 0x3d // Round To Double Grid opMIAP0 = 0x3e opMIAP1 = 0x3f @@ -202,38 +202,38 @@ const ( opPUSHW101 = 0xbd // . opPUSHW110 = 0xbe // . opPUSHW111 = 0xbf // . - opMDRP00000 = 0xc0 - opMDRP00001 = 0xc1 - opMDRP00010 = 0xc2 - opMDRP00011 = 0xc3 - opMDRP00100 = 0xc4 - opMDRP00101 = 0xc5 - opMDRP00110 = 0xc6 - opMDRP00111 = 0xc7 - opMDRP01000 = 0xc8 - opMDRP01001 = 0xc9 - opMDRP01010 = 0xca - opMDRP01011 = 0xcb - opMDRP01100 = 0xcc - opMDRP01101 = 0xcd - opMDRP01110 = 0xce - opMDRP01111 = 0xcf - opMDRP10000 = 0xd0 - opMDRP10001 = 0xd1 - opMDRP10010 = 0xd2 - opMDRP10011 = 0xd3 - opMDRP10100 = 0xd4 - opMDRP10101 = 0xd5 - opMDRP10110 = 0xd6 - opMDRP10111 = 0xd7 - opMDRP11000 = 0xd8 - opMDRP11001 = 0xd9 - opMDRP11010 = 0xda - opMDRP11011 = 0xdb - opMDRP11100 = 0xdd - opMDRP11101 = 0xdc - opMDRP11110 = 0xde - opMDRP11111 = 0xdf + opMDRP00000 = 0xc0 // Move Direct Relative Point + opMDRP00001 = 0xc1 // . + opMDRP00010 = 0xc2 // . + opMDRP00011 = 0xc3 // . + opMDRP00100 = 0xc4 // . + opMDRP00101 = 0xc5 // . + opMDRP00110 = 0xc6 // . + opMDRP00111 = 0xc7 // . + opMDRP01000 = 0xc8 // . + opMDRP01001 = 0xc9 // . + opMDRP01010 = 0xca // . + opMDRP01011 = 0xcb // . + opMDRP01100 = 0xcc // . + opMDRP01101 = 0xcd // . + opMDRP01110 = 0xce // . + opMDRP01111 = 0xcf // . + opMDRP10000 = 0xd0 // . + opMDRP10001 = 0xd1 // . + opMDRP10010 = 0xd2 // . + opMDRP10011 = 0xd3 // . + opMDRP10100 = 0xd4 // . + opMDRP10101 = 0xd5 // . + opMDRP10110 = 0xd6 // . + opMDRP10111 = 0xd7 // . + opMDRP11000 = 0xd8 // . + opMDRP11001 = 0xd9 // . + opMDRP11010 = 0xda // . + opMDRP11011 = 0xdb // . + opMDRP11100 = 0xdc // . + opMDRP11101 = 0xdd // . + opMDRP11110 = 0xde // . + opMDRP11111 = 0xdf // . opMIRP00000 = 0xe0 opMIRP00001 = 0xe1 opMIRP00010 = 0xe2 @@ -262,8 +262,8 @@ const ( opMIRP11001 = 0xf9 opMIRP11010 = 0xfa opMIRP11011 = 0xfb - opMIRP11100 = 0xfd - opMIRP11101 = 0xfc + opMIRP11100 = 0xfc + opMIRP11101 = 0xfd opMIRP11110 = 0xfe opMIRP11111 = 0xff ) @@ -273,8 +273,8 @@ var popCount = [256]uint8{ // 1, 2, 3, 4, 5, 6, 7, 8, 9, a, b, c, d, e, f 0, 0, 0, 0, 0, 0, q, q, q, q, 2, 2, 0, 0, 0, q, // 0x00 - 0x0f 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, // 0x10 - 0x1f - 1, 1, 0, 2, 0, 1, 1, q, q, q, 2, 1, 1, 0, q, q, // 0x20 - 0x2f - q, q, q, q, q, q, q, q, q, q, q, q, q, 0, q, q, // 0x30 - 0x3f + 1, 1, 0, 2, 0, 1, 1, q, q, q, 2, 1, 1, 0, 1, 1, // 0x20 - 0x2f + q, q, q, q, q, q, q, q, q, q, q, q, 0, 0, q, q, // 0x30 - 0x3f 0, 0, 2, 1, q, q, q, q, q, q, q, 0, 0, 0, 0, 0, // 0x40 - 0x4f 2, 2, 2, 2, 2, 2, 1, 1, 1, 0, 2, 2, 1, q, 1, 1, // 0x50 - 0x5f 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x60 - 0x6f @@ -283,8 +283,8 @@ var popCount = [256]uint8{ q, q, q, q, q, q, q, q, q, q, q, q, q, q, q, q, // 0x90 - 0x9f q, q, q, q, q, q, q, q, q, q, q, q, q, q, q, q, // 0xa0 - 0xaf 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xb0 - 0xbf - q, q, q, q, q, q, q, q, q, q, q, q, q, q, q, q, // 0xc0 - 0xcf - q, q, q, q, q, q, q, q, q, q, q, q, q, q, q, q, // 0xd0 - 0xdf + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0xc0 - 0xcf + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0xd0 - 0xdf q, q, q, q, q, q, q, q, q, q, q, q, q, q, q, q, // 0xe0 - 0xef q, q, q, q, q, q, q, q, q, q, q, q, q, q, q, q, // 0xf0 - 0xff } diff --git a/freetype/truetype/truetype_test.go b/freetype/truetype/truetype_test.go index 0136dfd..40be3f9 100644 --- a/freetype/truetype/truetype_test.go +++ b/freetype/truetype/truetype_test.go @@ -74,7 +74,7 @@ func TestParse(t *testing.T) { } } -func testScaling(t *testing.T, filename string) { +func testScaling(t *testing.T, filename string, hinter *Hinter) { b, err := ioutil.ReadFile("../../luxi-fonts/luxisr.ttf") if err != nil { t.Fatalf("ReadFile: %v", err) @@ -114,7 +114,14 @@ func testScaling(t *testing.T, filename string) { const fontSize = 12 glyphBuf := NewGlyphBuf() for i, want := range wants { - if err = glyphBuf.Load(font, fontSize*64, Index(i), nil); err != nil { + // TODO: completely implement hinting. For now, only the first N glyphs + // of luxisr.ttf are correctly hinted. + const N = 1 + if hinter != nil && i == N { + break + } + + if err = glyphBuf.Load(font, fontSize*64, Index(i), hinter); err != nil { t.Fatalf("Load: %v", err) } got := glyphBuf.Point @@ -128,9 +135,9 @@ func testScaling(t *testing.T, filename string) { } func TestScalingSansHinting(t *testing.T) { - testScaling(t, "luxisr-12pt-sans-hinting.txt") + testScaling(t, "luxisr-12pt-sans-hinting.txt", nil) } func TestScalingWithHinting(t *testing.T) { - // TODO. + testScaling(t, "luxisr-12pt-with-hinting.txt", &Hinter{}) }