diff --git a/freetype/truetype/glyph.go b/freetype/truetype/glyph.go index 52ebaa7..3688670 100644 --- a/freetype/truetype/glyph.go +++ b/freetype/truetype/glyph.go @@ -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) diff --git a/freetype/truetype/hint.go b/freetype/truetype/hint.go index 42b5617..864ac5f 100644 --- a/freetype/truetype/hint.go +++ b/freetype/truetype/hint.go @@ -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) diff --git a/freetype/truetype/opcodes.go b/freetype/truetype/opcodes.go index bd5ada5..12693c0 100644 --- a/freetype/truetype/opcodes.go +++ b/freetype/truetype/opcodes.go @@ -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. diff --git a/freetype/truetype/truetype_test.go b/freetype/truetype/truetype_test.go index 40be3f9..afbfd0a 100644 --- a/freetype/truetype/truetype_test.go +++ b/freetype/truetype/truetype_test.go @@ -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 }