From ab106efa010d2bf004ae4caa6e9078c215ba0d02 Mon Sep 17 00:00:00 2001 From: Nigel Tao Date: Thu, 24 Oct 2013 10:47:50 +1100 Subject: [PATCH] freetype/truetype: implement SPVTL, SFVTL, SHZ, SHPIX opcodes. Fix MIRP opcodes, scaled CVT initialization and Hinter.move. R=bsiegert CC=golang-dev, remyoudompheng https://codereview.appspot.com/14930050 --- freetype/truetype/hint.go | 189 ++++++++++++++++++++++------- freetype/truetype/hint_test.go | 60 +++++++++ freetype/truetype/opcodes.go | 18 +-- freetype/truetype/truetype_test.go | 2 +- 4 files changed, 214 insertions(+), 55 deletions(-) diff --git a/freetype/truetype/hint.go b/freetype/truetype/hint.go index df7745a..2afaab0 100644 --- a/freetype/truetype/hint.go +++ b/freetype/truetype/hint.go @@ -10,6 +10,7 @@ package truetype import ( "errors" + "math" ) const ( @@ -146,6 +147,7 @@ func (h *Hinter) init(f *Font, scale int32) error { if rescale { h.scale = scale + h.scaledCVTInitialized = false h.defaultGS = globalDefaultGS @@ -173,7 +175,6 @@ func (h *Hinter) run(program []byte, pCurrent, pUnhinted, pInFontUnits []Point, h.points[glyphZone][unhinted] = pUnhinted h.points[glyphZone][inFontUnits] = pInFontUnits h.ends = ends - h.scaledCVTInitialized = false if len(program) > 50000 { return errors.New("truetype: hinting: too many instructions") @@ -224,6 +225,29 @@ func (h *Hinter) run(program []byte, pCurrent, pUnhinted, pInFontUnits []Point, case opSFVTCA1: h.gs.fv = [2]f2dot14{0x4000, 0} + case opSPVTL0, opSPVTL1, opSFVTL0, opSFVTL1: + top -= 2 + p1 := h.point(0, current, h.stack[top+0]) + p2 := h.point(0, current, h.stack[top+1]) + if p1 == nil || p2 == nil { + return errors.New("truetype: hinting: point out of range") + } + dx := f2dot14(p1.X - p2.X) + dy := f2dot14(p1.Y - p2.Y) + if dx == 0 && dy == 0 { + dx = 0x4000 + } else if opcode&1 != 0 { + // Counter-clockwise rotation. + dx, dy = -dy, dx + } + v := normalize(dx, dy) + if opcode < opSFVTL0 { + h.gs.pv = v + h.gs.dv = v + } else { + h.gs.fv = v + } + case opSPVFS: top -= 2 h.gs.pv[0] = f2dot14(h.stack[top+0]) @@ -418,7 +442,7 @@ func (h *Hinter) run(program []byte, pCurrent, pUnhinted, pInFontUnits []Point, // TODO: metrics compensation. distance = h.round(distance) - distance } - h.move(p, distance) + h.move(p, distance, true) h.gs.rp[0] = i h.gs.rp[1] = i @@ -456,6 +480,41 @@ func (h *Hinter) run(program []byte, pCurrent, pUnhinted, pInFontUnits []Point, prevEnd = end } + case opSHZ0, opSHZ1: + top-- + zonePointer, i, d, ok := h.displacement(opcode&1 == 0) + if !ok { + return errors.New("truetype: hinting: point out of range") + } + + // As per C Freetype, SHZ doesn't move the phantom points, or mark + // the points as touched. + limit := int32(len(h.points[h.gs.zp[2]][current])) + if h.gs.zp[2] == glyphZone { + limit -= 4 + } + for j := int32(0); j < limit; j++ { + if i != j || h.gs.zp[zonePointer] != h.gs.zp[2] { + h.move(h.point(2, current, j), d, false) + } + } + + case opSHPIX: + top-- + d := f26dot6(h.stack[top]) + if top < int(h.gs.loop) { + return errors.New("truetype: hinting: stack underflow") + } + for ; h.gs.loop != 0; h.gs.loop-- { + top-- + p := h.point(2, current, h.stack[top]) + if p == nil { + return errors.New("truetype: hinting: point out of range") + } + h.move(p, d, true) + } + h.gs.loop = 1 + case opIP: if top < int(h.gs.loop) { return errors.New("truetype: hinting: stack underflow") @@ -472,9 +531,6 @@ func (h *Hinter) run(program []byte, pCurrent, pUnhinted, pInFontUnits []Point, p = h.point(1, current, h.gs.rp[2]) curP := h.point(0, current, h.gs.rp[1]) curRange := dotProduct(f26dot6(p.X-curP.X), f26dot6(p.Y-curP.Y), h.gs.pv) - if oldRange < 0 { - oldRange, curRange = -oldRange, -curRange - } for ; h.gs.loop != 0; h.gs.loop-- { top-- i := h.stack[top] @@ -485,19 +541,12 @@ func (h *Hinter) run(program []byte, pCurrent, pUnhinted, pInFontUnits []Point, newDist := f26dot6(0) if oldDist != 0 { if oldRange != 0 { - // Compute and round oldDist * curRange / oldRange. - x := int64(oldDist) * int64(curRange) - if x < 0 { - x -= int64(oldRange) / 2 - } else { - x += int64(oldRange) / 2 - } - newDist = f26dot6(x / int64(oldRange)) + newDist = f26dot6(mulDiv(int64(oldDist), int64(curRange), int64(oldRange))) } else { newDist = -oldDist } } - h.move(p, newDist-curDist) + h.move(p, newDist-curDist, true) } h.gs.loop = 1 @@ -515,7 +564,7 @@ func (h *Hinter) run(program []byte, pCurrent, pUnhinted, pInFontUnits []Point, if p == nil { return errors.New("truetype: hinting: point out of range") } - h.move(p, -dotProduct(f26dot6(p.X-ref.X), f26dot6(p.Y-ref.Y), h.gs.pv)) + h.move(p, -dotProduct(f26dot6(p.X-ref.X), f26dot6(p.Y-ref.Y), h.gs.pv), true) } h.gs.loop = 1 @@ -544,7 +593,7 @@ func (h *Hinter) run(program []byte, pCurrent, pUnhinted, pInFontUnits []Point, // TODO: metrics compensation. distance = h.round(distance) } - h.move(p, distance-oldDist) + h.move(p, distance-oldDist, true) h.gs.rp[0] = i h.gs.rp[1] = i @@ -874,7 +923,7 @@ func (h *Hinter) run(program []byte, pCurrent, pUnhinted, pInFontUnits []Point, // Move the point. oldDist = dotProduct(f26dot6(p.X-ref.X), f26dot6(p.Y-ref.Y), h.gs.pv) - h.move(p, distance-oldDist) + h.move(p, distance-oldDist, true) case opMIRP00000, opMIRP00001, opMIRP00010, opMIRP00011, opMIRP00100, opMIRP00101, opMIRP00110, opMIRP00111, @@ -914,7 +963,7 @@ func (h *Hinter) run(program []byte, pCurrent, pUnhinted, pInFontUnits []Point, if ref == nil || p == nil { return errors.New("truetype: hinting: point out of range") } - curDist := dotProduct(f26dot6(p.X-ref.X), f26dot6(p.Y-ref.Y), h.gs.dv) + curDist := dotProduct(f26dot6(p.X-ref.X), f26dot6(p.Y-ref.Y), h.gs.pv) if h.gs.autoFlip && oldDist^cvtDist < 0 { cvtDist = -cvtDist @@ -922,9 +971,9 @@ func (h *Hinter) run(program []byte, pCurrent, pUnhinted, pInFontUnits []Point, // Rounding bit. // TODO: metrics compensation. - distance := oldDist + distance := cvtDist if opcode&0x04 != 0 { - distance = h.round(oldDist) + distance = h.round(cvtDist) } // Minimum distance bit. @@ -948,7 +997,7 @@ func (h *Hinter) run(program []byte, pCurrent, pUnhinted, pInFontUnits []Point, h.gs.rp[2] = i // Move the point. - h.move(p, distance-curDist) + h.move(p, distance-curDist, true) default: return errors.New("truetype: hinting: unrecognized instruction") @@ -1077,25 +1126,42 @@ func (h *Hinter) point(zonePointer uint32, pt pointType, i int32) *Point { return &points[i] } -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 - } +func (h *Hinter) move(p *Point, distance f26dot6, touch bool) { fvx := int64(h.gs.fv[0]) - fvy := int64(h.gs.fv[1]) pvx := int64(h.gs.pv[0]) + if fvx == 0x4000 && pvx == 0x4000 { + p.X += int32(distance) + if touch { + p.Flags |= flagTouchedX + } + return + } + + fvy := int64(h.gs.fv[1]) pvy := int64(h.gs.pv[1]) + if fvy == 0x4000 && pvy == 0x4000 { + p.Y += int32(distance) + if touch { + p.Flags |= flagTouchedY + } + return + } + fvDotPv := (fvx*pvx + fvy*pvy) >> 14 - p.X += int32(int64(distance) * fvx / fvDotPv) - p.Y += int32(int64(distance) * fvy / fvDotPv) - p.Flags |= flagTouchedX | flagTouchedY + + if fvx != 0 { + p.X += int32(mulDiv(fvx, int64(distance), fvDotPv)) + if touch { + p.Flags |= flagTouchedX + } + } + + if fvy != 0 { + p.Y += int32(mulDiv(fvy, int64(distance), fvDotPv)) + if touch { + p.Flags |= flagTouchedY + } + } } func (h *Hinter) iupInterp(interpY bool, p1, p2, ref1, ref2 int) { @@ -1174,14 +1240,7 @@ func (h *Hinter) iupInterp(interpY bool, p1, p2, ref1, ref2 int) { } else { if !scaleOK { scaleOK = true - numer := int64(unh2+delta2-unh1-delta1) * 0x10000 - denom := int64(ifu2 - ifu1) - if numer >= 0 { - numer += denom / 2 - } else { - numer -= denom / 2 - } - scale = numer / denom + scale = mulDiv(int64(unh2+delta2-unh1-delta1), 0x10000, int64(ifu2-ifu1)) } numer := int64(ifuXY-ifu1) * scale if numer >= 0 { @@ -1222,6 +1281,20 @@ func (h *Hinter) iupShift(interpY bool, p1, p2, p int) { } } +func (h *Hinter) displacement(useZP1 bool) (zonePointer uint32, i int32, d f26dot6, ok bool) { + zonePointer, i = uint32(0), h.gs.rp[1] + if useZP1 { + zonePointer, i = 1, h.gs.rp[2] + } + p := h.point(zonePointer, current, i) + q := h.point(zonePointer, unhinted, i) + if p == nil || q == nil { + return 0, 0, 0, false + } + d = dotProduct(f26dot6(p.X-q.X), f26dot6(p.Y-q.Y), h.gs.pv) + return zonePointer, i, d, true +} + // 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) { @@ -1251,6 +1324,17 @@ func skipInstructionPayload(program []byte, pc int) (newPC int, ok bool) { // f2dot14 is a 2.14 fixed point number. type f2dot14 int16 +func normalize(x, y f2dot14) [2]f2dot14 { + fx, fy := float64(x), float64(y) + l := math.Hypot(fx, fy) + fx /= l + fy /= l + return [2]f2dot14{ + f2dot14(fx * 0x4000), + f2dot14(fy * 0x4000), + } +} + // f26dot6 is a 26.6 fixed point number. type f26dot6 int32 @@ -1272,12 +1356,27 @@ func (x f26dot6) mul(y f26dot6) f26dot6 { return f26dot6(int64(x) * int64(y) >> 6) } +// dotProduct returns the dot product of [x, y] and q. 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) + return f26dot6((px*qx + py*qy + 1<<13) >> 14) +} + +// mulDiv returns x*y/z, rounded to the nearest integer. +func mulDiv(x, y, z int64) int64 { + xy := x * y + if z < 0 { + xy, z = -xy, -z + } + if xy >= 0 { + xy += z / 2 + } else { + xy -= z / 2 + } + return xy / z } // round rounds the given number. The rounding algorithm is described at diff --git a/freetype/truetype/hint_test.go b/freetype/truetype/hint_test.go index 17fcaa6..98d9453 100644 --- a/freetype/truetype/hint_test.go +++ b/freetype/truetype/hint_test.go @@ -582,3 +582,63 @@ func TestBytecode(t *testing.T) { } } } + +func TestMove(t *testing.T) { + h, p := Hinter{}, Point{} + testCases := []struct { + pvX, pvY, fvX, fvY f2dot14 + wantX, wantY int32 + }{ + {+0x4000, +0x0000, +0x4000, +0x0000, +1000, +0}, + {+0x4000, +0x0000, -0x4000, +0x0000, +1000, +0}, + {-0x4000, +0x0000, +0x4000, +0x0000, -1000, +0}, + {-0x4000, +0x0000, -0x4000, +0x0000, -1000, +0}, + {+0x0000, +0x4000, +0x0000, +0x4000, +0, +1000}, + {+0x0000, +0x4000, +0x0000, -0x4000, +0, +1000}, + {+0x4000, +0x0000, +0x2d41, +0x2d41, +1000, +1000}, + {+0x4000, +0x0000, -0x2d41, +0x2d41, +1000, -1000}, + {+0x4000, +0x0000, +0x2d41, -0x2d41, +1000, -1000}, + {+0x4000, +0x0000, -0x2d41, -0x2d41, +1000, +1000}, + {-0x4000, +0x0000, +0x2d41, +0x2d41, -1000, -1000}, + {-0x4000, +0x0000, -0x2d41, +0x2d41, -1000, +1000}, + {-0x4000, +0x0000, +0x2d41, -0x2d41, -1000, +1000}, + {-0x4000, +0x0000, -0x2d41, -0x2d41, -1000, -1000}, + {+0x376d, +0x2000, +0x2d41, +0x2d41, +732, +732}, + {-0x376d, +0x2000, +0x2d41, +0x2d41, -2732, -2732}, + {+0x376d, +0x2000, +0x2d41, -0x2d41, +2732, -2732}, + {-0x376d, +0x2000, +0x2d41, -0x2d41, -732, +732}, + {-0x376d, -0x2000, +0x2d41, +0x2d41, -732, -732}, + {+0x376d, +0x2000, +0x4000, +0x0000, +1155, +0}, + {+0x376d, +0x2000, +0x0000, +0x4000, +0, +2000}, + } + for _, tc := range testCases { + p = Point{} + h.gs.pv = [2]f2dot14{tc.pvX, tc.pvY} + h.gs.fv = [2]f2dot14{tc.fvX, tc.fvY} + h.move(&p, 1000, true) + tx := p.Flags&flagTouchedX != 0 + ty := p.Flags&flagTouchedY != 0 + wantTX := tc.fvX != 0 + wantTY := tc.fvY != 0 + if p.X != tc.wantX || p.Y != tc.wantY || tx != wantTX || ty != wantTY { + t.Errorf("pv=%v, fv=%v\ngot %d, %d, %t, %t\nwant %d, %d, %t, %t", + h.gs.pv, h.gs.fv, p.X, p.Y, tx, ty, tc.wantX, tc.wantY, wantTX, wantTY) + continue + } + + // Check that p is aligned with the freedom vector. + a := int64(p.X) * int64(tc.fvY) + b := int64(p.Y) * int64(tc.fvX) + if a != b { + t.Errorf("pv=%v, fv=%v, p=%v not aligned with fv", h.gs.pv, h.gs.fv, p) + continue + } + + // Check that the projected p is 1000 away from the origin. + dotProd := (int64(p.X)*int64(tc.pvX) + int64(p.Y)*int64(tc.pvY) + 1<<13) >> 14 + if dotProd != 1000 { + t.Errorf("pv=%v, fv=%v, p=%v not 1000 from origin", h.gs.pv, h.gs.fv, p) + continue + } + } +} diff --git a/freetype/truetype/opcodes.go b/freetype/truetype/opcodes.go index 7b16e5d..e6b9e70 100644 --- a/freetype/truetype/opcodes.go +++ b/freetype/truetype/opcodes.go @@ -16,10 +16,10 @@ const ( opSPVTCA1 = 0x03 // . opSFVTCA0 = 0x04 // Set Freedom Vector to Coordinate Axis opSFVTCA1 = 0x05 // . - opSPVTL0 = 0x06 - opSPVTL1 = 0x07 - opSFVTL0 = 0x08 - opSFVTL1 = 0x09 + opSPVTL0 = 0x06 // Set Projection Vector To Line + opSPVTL1 = 0x07 // . + opSFVTL0 = 0x08 // Set Freedom Vector To Line + opSFVTL1 = 0x09 // . opSPVFS = 0x0a // Set Projection Vector From Stack opSFVFS = 0x0b // Set Freedom Vector From Stack opGPV = 0x0c // Get Projection Vector @@ -64,9 +64,9 @@ const ( opSHP1 = 0x33 opSHC0 = 0x34 opSHC1 = 0x35 - opSHZ0 = 0x36 - opSHZ1 = 0x37 - opSHPIX = 0x38 + opSHZ0 = 0x36 // SHift Zone using reference point + opSHZ1 = 0x37 // . + opSHPIX = 0x38 // SHift point by a PIXel amount opIP = 0x39 // Interpolate Point opMSIRP0 = 0x3a opMSIRP1 = 0x3b @@ -271,10 +271,10 @@ const ( // popCount is the number of stack elements that each opcode pops. 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 + 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 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, 1, 1, // 0x20 - 0x2f - 0, 0, q, q, q, q, q, q, q, 0, q, q, 0, 0, 2, 2, // 0x30 - 0x3f + 0, 0, q, q, q, q, 1, 1, 1, 0, q, q, 0, 0, 2, 2, // 0x30 - 0x3f 0, 0, 2, 1, 2, 1, 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 diff --git a/freetype/truetype/truetype_test.go b/freetype/truetype/truetype_test.go index 2a29765..b391db3 100644 --- a/freetype/truetype/truetype_test.go +++ b/freetype/truetype/truetype_test.go @@ -253,7 +253,7 @@ var scalingTestCases = []struct { }{ {"luxisr", 12, -1}, {"x-arial-bold", 11, 0}, - {"x-deja-vu-sans-oblique", 17, 1}, + {"x-deja-vu-sans-oblique", 17, 5}, {"x-droid-sans-japanese", 9, 0}, {"x-times-new-roman", 13, 0}, }