freetype/truetype: implement IP, MIAP, MIRP opcodes.

R=bsiegert
CC=golang-dev
https://codereview.appspot.com/13855043
This commit is contained in:
Nigel Tao 2013-09-24 22:06:47 +10:00
parent a3c53fdc3f
commit 75fff80b59
4 changed files with 294 additions and 96 deletions

View file

@ -17,16 +17,15 @@ 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 {
// The glyph's bounding box.
// B is the glyph's bounding box.
B Bounds
// 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
// Points before they were hinted and scaled.
Point, Unhinted, InFontUnits []Point
// End is the point indexes of the end point of each countour. 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.
End []int
@ -123,7 +122,6 @@ func (g *GlyphBuf) Load(f *Font, scale int32, i Index, h *Hinter) error {
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 h != nil {
if err := h.init(g, f, scale); err != nil {
@ -285,13 +283,6 @@ func (g *GlyphBuf) load(f *Font, scale int32, i Index, h *Hinter,
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)

View file

@ -40,6 +40,9 @@ type Hinter struct {
// default graphics state is the global default graphics state after
// the font's fpgm and prep programs have been run.
gs, defaultGS graphicsState
// twilightXxx are points created in the twilight zone.
twilightPoint, twilightUnhinted, twilightInFontUnits []Point
}
// graphicsState is described at https://developer.apple.com/fonts/TTRefMan/RM04/Chap4.html
@ -77,8 +80,25 @@ var globalDefaultGS = graphicsState{
autoFlip: true,
}
func resetTwilightPoints(f *Font, p []Point) []Point {
// TODO: the C Freetype code uses n+4 for the 4 phantom points, but a
// comment there says "(do we need this?)". Do we need to use n+4 here?
if n := int(f.maxTwilightPoints); n <= cap(p) {
p = p[:n]
for i := range p {
p[i] = Point{}
}
} else {
p = make([]Point, n)
}
return p
}
func (h *Hinter) init(g *GlyphBuf, f *Font, scale int32) error {
h.g = g
h.twilightPoint = resetTwilightPoints(f, h.twilightPoint)
h.twilightUnhinted = resetTwilightPoints(f, h.twilightUnhinted)
h.twilightInFontUnits = resetTwilightPoints(f, h.twilightInFontUnits)
rescale := h.scale != scale
if h.font != f {
@ -365,13 +385,12 @@ 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 {
i := h.stack[top]
p := h.point(0, current, i)
if p == nil {
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)
@ -379,23 +398,73 @@ func (h *Hinter) run(program []byte) error {
distance = h.round(distance) - distance
}
h.move(p, distance)
h.gs.rp[0] = int32(i)
h.gs.rp[1] = int32(i)
h.gs.rp[0] = i
h.gs.rp[1] = i
case opIUP0, opIUP1:
// TODO: implement IUP.
// Glyph 4 in luxisr.ttf (the '!' glyph) has IUP instructions but
// they have no effect since none of the points are untouched.
// The IUP ops will be implemented when the N in truetype_test.go
// is raised above 5.
const mask = flagTouchedX | flagTouchedY
for _, p := range h.g.Point {
if p.Flags&mask != mask {
return errors.New("truetype: hinting: unimplemented IUP instruction")
}
}
case opIP:
if top < int(h.gs.loop) {
return errors.New("truetype: hinting: stack underflow")
}
pointType := inFontUnits
twilight := h.gs.zp[0] == 0 || h.gs.zp[1] == 0 || h.gs.zp[2] == 0
if twilight {
pointType = unhinted
}
p := h.point(1, pointType, h.gs.rp[2])
oldP := h.point(0, pointType, h.gs.rp[1])
oldRange := dotProduct(f26dot6(p.X-oldP.X), f26dot6(p.Y-oldP.Y), h.gs.dv)
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)
for ; h.gs.loop != 0; h.gs.loop-- {
top--
i := h.stack[top]
p = h.point(2, pointType, i)
oldDist := dotProduct(f26dot6(p.X-oldP.X), f26dot6(p.Y-oldP.Y), h.gs.dv)
p = h.point(2, current, i)
curDist := dotProduct(f26dot6(p.X-curP.X), f26dot6(p.Y-curP.Y), h.gs.pv)
newDist := f26dot6(0)
if oldDist != 0 {
if oldRange != 0 {
newDist = f26dot6(
(int64(oldDist)*int64(curRange) + int64(oldRange/2)) / int64(oldRange))
} else {
newDist = -oldDist
}
}
h.move(p, newDist-curDist)
}
h.gs.loop = 1
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 {
i := h.gs.rp[0]
ref := h.point(0, current, i)
if ref == nil {
return errors.New("truetype: hinting: point out of range")
}
ref := &points[i]
points = h.g.points(h.gs.zp[1])
points := h.points(1, current)
for ; h.gs.loop != 0; h.gs.loop-- {
top--
i = int(h.stack[top])
if i < 0 || len(points) <= i {
i = h.stack[top]
if i < 0 || len(points) <= int(i) {
return errors.New("truetype: hinting: point out of range")
}
p := &points[i]
@ -408,6 +477,30 @@ func (h *Hinter) run(program []byte) error {
h.gs.roundPhase = 0
h.gs.roundThreshold = 1 << 4
case opMIAP0, opMIAP1:
top -= 2
i := h.stack[top]
distance := h.cvt(h.stack[top+1])
if h.gs.zp[0] == 0 {
p := h.point(0, unhinted, i)
q := h.point(0, current, i)
p.X = int32((int64(distance) * int64(h.gs.fv[0])) >> 14)
p.Y = int32((int64(distance) * int64(h.gs.fv[1])) >> 14)
*q = *p
}
p := h.point(0, current, i)
oldDist := dotProduct(f26dot6(p.X), f26dot6(p.Y), h.gs.pv)
if opcode == opMIAP1 {
if (distance - oldDist).abs() > h.gs.controlValueCutIn {
distance = oldDist
}
// TODO: metrics compensation.
distance = h.round(distance)
}
h.move(p, distance-oldDist)
h.gs.rp[0] = i
h.gs.rp[1] = i
case opNPUSHB:
opcode = 0
goto push
@ -669,50 +762,45 @@ func (h *Hinter) run(program []byte) error {
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 {
i := h.stack[top]
ref := h.point(0, current, h.gs.rp[0])
p := h.point(1, current, i)
if ref == nil || p == nil {
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)
oldDist := f26dot6(0)
if h.gs.zp[0] == 0 || h.gs.zp[1] == 0 {
p0 := h.point(1, unhinted, i)
p1 := h.point(0, unhinted, h.gs.rp[0])
oldDist = 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)))
p0 := h.point(1, inFontUnits, i)
p1 := h.point(0, inFontUnits, h.gs.rp[0])
oldDist = dotProduct(f26dot6(p0.X-p1.X), f26dot6(p0.Y-p1.Y), h.gs.dv)
oldDist = f26dot6(h.font.scale(h.scale * int32(oldDist)))
}
// Single-width cut-in test.
if x := (origDist - h.gs.singleWidth).abs(); x < h.gs.singleWidthCutIn {
if origDist >= 0 {
origDist = h.gs.singleWidthCutIn
if x := (oldDist - h.gs.singleWidth).abs(); x < h.gs.singleWidthCutIn {
if oldDist >= 0 {
oldDist = h.gs.singleWidthCutIn
} else {
origDist = -h.gs.singleWidthCutIn
oldDist = -h.gs.singleWidthCutIn
}
}
// Rounding bit.
// TODO: metrics compensation.
distance := origDist
distance := oldDist
if opcode&0x04 != 0 {
distance = h.round(origDist)
distance = h.round(oldDist)
}
// Minimum distance bit.
if opcode&0x08 != 0 {
if origDist >= 0 {
if oldDist >= 0 {
if distance < h.gs.minDist {
distance = h.gs.minDist
}
@ -725,14 +813,88 @@ func (h *Hinter) run(program []byte) error {
// Set-RP0 bit.
if opcode&0x10 != 0 {
h.gs.rp[0] = int32(i)
h.gs.rp[0] = i
}
h.gs.rp[1] = h.gs.rp[0]
h.gs.rp[2] = int32(i)
h.gs.rp[2] = i
// Move the point.
origDist = dotProduct(f26dot6(p.X-ref.X), f26dot6(p.Y-ref.Y), h.gs.pv)
h.move(p, distance-origDist)
oldDist = dotProduct(f26dot6(p.X-ref.X), f26dot6(p.Y-ref.Y), h.gs.pv)
h.move(p, distance-oldDist)
case opMIRP00000, opMIRP00001, opMIRP00010, opMIRP00011,
opMIRP00100, opMIRP00101, opMIRP00110, opMIRP00111,
opMIRP01000, opMIRP01001, opMIRP01010, opMIRP01011,
opMIRP01100, opMIRP01101, opMIRP01110, opMIRP01111,
opMIRP10000, opMIRP10001, opMIRP10010, opMIRP10011,
opMIRP10100, opMIRP10101, opMIRP10110, opMIRP10111,
opMIRP11000, opMIRP11001, opMIRP11010, opMIRP11011,
opMIRP11100, opMIRP11101, opMIRP11110, opMIRP11111:
top -= 2
i := h.stack[top]
cvtDist := h.cvt(h.stack[top+1])
if (cvtDist - h.gs.singleWidth).abs() < h.gs.singleWidthCutIn {
if cvtDist >= 0 {
cvtDist = +h.gs.singleWidth
} else {
cvtDist = -h.gs.singleWidth
}
}
if h.gs.zp[1] == 0 {
// TODO: implement once we have a .ttf file that triggers
// this, so that we can step through C's freetype.
return errors.New("truetype: hinting: unimplemented twilight point adjustment")
}
ref := h.point(0, unhinted, h.gs.rp[0])
p := h.point(1, unhinted, i)
if ref == nil || p == nil {
return errors.New("truetype: hinting: point out of range")
}
oldDist := dotProduct(f26dot6(p.X-ref.X), f26dot6(p.Y-ref.Y), h.gs.dv)
ref = h.point(0, current, h.gs.rp[0])
p = h.point(1, current, i)
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)
if h.gs.autoFlip && oldDist^cvtDist < 0 {
cvtDist = -cvtDist
}
// Rounding bit.
// TODO: metrics compensation.
distance := oldDist
if opcode&0x04 != 0 {
distance = h.round(oldDist)
}
// Minimum distance bit.
if opcode&0x08 != 0 {
if oldDist >= 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] = i
}
h.gs.rp[1] = h.gs.rp[0]
h.gs.rp[2] = i
// Move the point.
h.move(p, distance-curDist)
default:
return errors.New("truetype: hinting: unrecognized instruction")
@ -815,6 +977,51 @@ func (h *Hinter) run(program []byte) error {
return nil
}
// cvt returns the scaled value from the font's Control Value Table.
func (h *Hinter) cvt(i int32) f26dot6 {
i *= 2
if i < 0 || len(h.font.cvt) < int(i) {
return 0
}
cv := uint16(h.font.cvt[i])<<8 | uint16(h.font.cvt[i+1])
return f26dot6(h.font.scale(h.scale * int32(int16(cv))))
}
type pointType uint32
const (
current pointType = 0
unhinted pointType = 1
inFontUnits pointType = 2
)
func (h *Hinter) point(zone uint32, pt pointType, i int32) *Point {
points := h.points(zone, pt)
if i < 0 || len(points) <= int(i) {
return nil
}
return &points[i]
}
func (h *Hinter) points(zone uint32, pt pointType) []Point {
if h.gs.zp[zone] == 0 {
switch pt {
case unhinted:
return h.twilightUnhinted
case inFontUnits:
return h.twilightInFontUnits
}
return h.twilightPoint
}
switch pt {
case unhinted:
return h.g.Unhinted
case inFontUnits:
return h.g.InFontUnits
}
return h.g.Point
}
func (h *Hinter) move(p *Point, distance f26dot6) {
if h.gs.fv[0] == 0 {
p.Y += int32(distance)

View file

@ -58,8 +58,8 @@ const (
opENDF = 0x2d // END Function definition
opMDAP0 = 0x2e // Move Direct Absolute Point
opMDAP1 = 0x2f // .
opIUP0 = 0x30
opIUP1 = 0x31
opIUP0 = 0x30 // Interpolate Untouched Points through the outline
opIUP1 = 0x31 // .
opSHP0 = 0x32
opSHP1 = 0x33
opSHC0 = 0x34
@ -67,13 +67,13 @@ const (
opSHZ0 = 0x36
opSHZ1 = 0x37
opSHPIX = 0x38
opIP = 0x39
opIP = 0x39 // Interpolate Point
opMSIRP0 = 0x3a
opMSIRP1 = 0x3b
opALIGNRP = 0x3c // ALIGN to Reference Point
opRTDG = 0x3d // Round To Double Grid
opMIAP0 = 0x3e
opMIAP1 = 0x3f
opMIAP0 = 0x3e // Move Indirect Absolute Point
opMIAP1 = 0x3f // .
opNPUSHB = 0x40 // PUSH N Bytes
opNPUSHW = 0x41 // PUSH N Words
opWS = 0x42 // Write Store
@ -234,38 +234,38 @@ const (
opMDRP11101 = 0xdd // .
opMDRP11110 = 0xde // .
opMDRP11111 = 0xdf // .
opMIRP00000 = 0xe0
opMIRP00001 = 0xe1
opMIRP00010 = 0xe2
opMIRP00011 = 0xe3
opMIRP00100 = 0xe4
opMIRP00101 = 0xe5
opMIRP00110 = 0xe6
opMIRP00111 = 0xe7
opMIRP01000 = 0xe8
opMIRP01001 = 0xe9
opMIRP01010 = 0xea
opMIRP01011 = 0xeb
opMIRP01100 = 0xec
opMIRP01101 = 0xed
opMIRP01110 = 0xee
opMIRP01111 = 0xef
opMIRP10000 = 0xf0
opMIRP10001 = 0xf1
opMIRP10010 = 0xf2
opMIRP10011 = 0xf3
opMIRP10100 = 0xf4
opMIRP10101 = 0xf5
opMIRP10110 = 0xf6
opMIRP10111 = 0xf7
opMIRP11000 = 0xf8
opMIRP11001 = 0xf9
opMIRP11010 = 0xfa
opMIRP11011 = 0xfb
opMIRP11100 = 0xfc
opMIRP11101 = 0xfd
opMIRP11110 = 0xfe
opMIRP11111 = 0xff
opMIRP00000 = 0xe0 // Move Indirect Relative Point
opMIRP00001 = 0xe1 // .
opMIRP00010 = 0xe2 // .
opMIRP00011 = 0xe3 // .
opMIRP00100 = 0xe4 // .
opMIRP00101 = 0xe5 // .
opMIRP00110 = 0xe6 // .
opMIRP00111 = 0xe7 // .
opMIRP01000 = 0xe8 // .
opMIRP01001 = 0xe9 // .
opMIRP01010 = 0xea // .
opMIRP01011 = 0xeb // .
opMIRP01100 = 0xec // .
opMIRP01101 = 0xed // .
opMIRP01110 = 0xee // .
opMIRP01111 = 0xef // .
opMIRP10000 = 0xf0 // .
opMIRP10001 = 0xf1 // .
opMIRP10010 = 0xf2 // .
opMIRP10011 = 0xf3 // .
opMIRP10100 = 0xf4 // .
opMIRP10101 = 0xf5 // .
opMIRP10110 = 0xf6 // .
opMIRP10111 = 0xf7 // .
opMIRP11000 = 0xf8 // .
opMIRP11001 = 0xf9 // .
opMIRP11010 = 0xfa // .
opMIRP11011 = 0xfb // .
opMIRP11100 = 0xfc // .
opMIRP11101 = 0xfd // .
opMIRP11110 = 0xfe // .
opMIRP11111 = 0xff // .
)
// popCount is the number of stack elements that each opcode pops.
@ -274,7 +274,7 @@ var popCount = [256]uint8{
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, 1, 1, // 0x20 - 0x2f
q, q, q, q, q, q, q, q, q, q, q, q, 0, 0, q, q, // 0x30 - 0x3f
0, 0, q, q, q, q, q, q, q, 0, q, q, 0, 0, 2, 2, // 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
@ -285,8 +285,8 @@ var popCount = [256]uint8{
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xb0 - 0xbf
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
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0xe0 - 0xef
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0xf0 - 0xff
}
// popCount[opcode] == q means that that opcode is not yet implemented.

View file

@ -116,7 +116,7 @@ func testScaling(t *testing.T, filename string, hinter *Hinter) {
for i, want := range wants {
// TODO: completely implement hinting. For now, only the first N glyphs
// of luxisr.ttf are correctly hinted.
const N = 1
const N = 5
if hinter != nil && i == N {
break
}