diff --git a/freetype/truetype/hint.go b/freetype/truetype/hint.go index 697a2b1..587935d 100644 --- a/freetype/truetype/hint.go +++ b/freetype/truetype/hint.go @@ -14,10 +14,16 @@ import ( type hinter struct { stack [800]int32 - // TODO: add more state, as per https://developer.apple.com/fonts/TTRefMan/RM04/Chap4.html + // The graphics state is described at https://developer.apple.com/fonts/TTRefMan/RM04/Chap4.html + roundPeriod, roundPhase, roundThreshold int32 } func (h *hinter) run(program []byte) error { + // The default rounding policy is round to grid. + h.roundPeriod = 1 << 6 + h.roundPhase = 0 + h.roundThreshold = 1 << 5 + if len(program) > 50000 { return errors.New("truetype: hinting: too many instructions") } @@ -39,6 +45,16 @@ func (h *hinter) run(program []byte) error { } switch opcode { + case opRTG: + h.roundPeriod = 1 << 6 + h.roundPhase = 0 + h.roundThreshold = 1 << 5 + + case opRTHG: + h.roundPeriod = 1 << 6 + h.roundPhase = 1 << 5 + h.roundThreshold = 1 << 5 + case opELSE: opcode = 1 goto ifelse @@ -82,6 +98,11 @@ func (h *hinter) run(program []byte) error { top-- } + case opRTDG: + h.roundPeriod = 1 << 5 + h.roundPhase = 0 + h.roundThreshold = 1 << 4 + case opNPUSHB: opcode = 0 goto push @@ -151,11 +172,11 @@ func (h *hinter) run(program []byte) error { if h.stack[top] == 0 { return errors.New("truetype: hinting: division by zero") } - h.stack[top-1] = int32((int64(h.stack[top-1]) << 6) / int64(h.stack[top])) + h.stack[top-1] = div(h.stack[top-1], h.stack[top]) case opMUL: top-- - h.stack[top-1] = int32((int64(h.stack[top-1]) * int64(h.stack[top])) >> 6) + h.stack[top-1] = mul(h.stack[top-1], h.stack[top]) case opABS: if h.stack[top-1] < 0 { @@ -172,6 +193,41 @@ func (h *hinter) run(program []byte) error { h.stack[top-1] += 63 h.stack[top-1] &^= 63 + case opROUND00, opROUND01, opROUND10, opROUND11: + // The four flavors of opROUND are equivalent. See the comment below on + // opNROUND for the rationale. + h.stack[top-1] = h.round(h.stack[top-1]) + + case opNROUND00, opNROUND01, opNROUND10, opNROUND11: + // No-op. The spec says to add one of four "compensations for the engine + // characteristics", to cater for things like "different dot-size printers". + // https://developer.apple.com/fonts/TTRefMan/RM02/Chap2.html#engine_compensation + // This code does not implement engine compensation, as we don't expect to + // be used to output on dot-matrix printers. + + case opSROUND, opS45ROUND: + top-- + switch (h.stack[top] >> 6) & 0x03 { + case 0: + h.roundPeriod = 1 << 5 + case 1, 3: + h.roundPeriod = 1 << 6 + case 2: + h.roundPeriod = 1 << 7 + } + if opcode == opS45ROUND { + // The spec says to multiply by √2, but the C Freetype code says 1/√2. + // We go with 1/√2. + h.roundPeriod *= 46341 + h.roundPeriod /= 65536 + } + h.roundPhase = h.roundPeriod * ((h.stack[top] >> 4) & 0x03) / 4 + if x := h.stack[top] & 0x0f; x != 0 { + h.roundThreshold = h.roundPeriod * (x - 4) / 8 + } else { + h.roundThreshold = h.roundPeriod - 1 + } + case opJROT: top -= 2 if h.stack[top+1] != 0 { @@ -186,6 +242,21 @@ func (h *hinter) run(program []byte) error { continue } + case opROFF: + h.roundPeriod = 0 + h.roundPhase = 0 + h.roundThreshold = 0 + + case opRUTG: + h.roundPeriod = 1 << 6 + h.roundPhase = 0 + h.roundThreshold = 1<<6 - 1 + + case opRDTG: + h.roundPeriod = 1 << 6 + h.roundPhase = 0 + h.roundThreshold = 0 + case opPUSHB000, opPUSHB001, opPUSHB010, opPUSHB011, opPUSHB100, opPUSHB101, opPUSHB110, opPUSHB111: opcode -= opPUSHB000 - 1 goto push @@ -288,6 +359,43 @@ func (h *hinter) run(program []byte) error { return nil } +// div returns x/y in 26.6 fixed point arithmetic. +func div(x, y int32) int32 { + return int32((int64(x) << 6) / int64(y)) +} + +// mul returns x*y in 26.6 fixed point arithmetic. +func mul(x, y int32) int32 { + return int32(int64(x) * int64(y) >> 6) +} + +// 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 int32) int32 { + if h.roundPeriod == 0 { + return x + } + neg := x < 0 + x -= h.roundPhase + x += h.roundThreshold + if x >= 0 { + x = (x / h.roundPeriod) * h.roundPeriod + } else { + x -= h.roundPeriod + x += 1 + x = (x / h.roundPeriod) * h.roundPeriod + } + x += h.roundPhase + if neg { + if x >= 0 { + x = h.roundPhase - h.roundPeriod + } + } else if x < 0 { + x = h.roundPhase + } + return x +} + func bool2int32(b bool) int32 { if b { return 1 diff --git a/freetype/truetype/hint_test.go b/freetype/truetype/hint_test.go index 4fa585d..94a7914 100644 --- a/freetype/truetype/hint_test.go +++ b/freetype/truetype/hint_test.go @@ -345,6 +345,79 @@ func TestBytecode(t *testing.T) { []int32{64, 128}, "", }, + { + "rounding", + // Round 1.40625 (which is 90/64) under various rounding policies. + // See figure 20 of https://developer.apple.com/fonts/TTRefMan/RM02/Chap2.html#rounding + []byte{ + opROFF, // [] + opPUSHB000, // [90] + 90, + opROUND00, // [90] + opRTG, // [90] + opPUSHB000, // [90, 90] + 90, + opROUND00, // [90, 64] + opRTHG, // [90, 64] + opPUSHB000, // [90, 64, 90] + 90, + opROUND00, // [90, 64, 96] + opRDTG, // [90, 64, 96] + opPUSHB000, // [90, 64, 96, 90] + 90, + opROUND00, // [90, 64, 96, 64] + opRUTG, // [90, 64, 96, 64] + opPUSHB000, // [90, 64, 96, 64, 90] + 90, + opROUND00, // [90, 64, 96, 64, 128] + opRTDG, // [90, 64, 96, 64, 128] + opPUSHB000, // [90, 64, 96, 64, 128, 90] + 90, + opROUND00, // [90, 64, 96, 64, 128, 96] + }, + []int32{90, 64, 96, 64, 128, 96}, + "", + }, + { + "super-rounding", + // See figure 20 of https://developer.apple.com/fonts/TTRefMan/RM02/Chap2.html#rounding + // and the sign preservation steps of the "Order of rounding operations" section. + []byte{ + opPUSHB000, // [0x58] + 0x58, + opSROUND, // [] + opPUSHW000, // [-81] + 0xff, + 0xaf, + opROUND00, // [-112] + opPUSHW000, // [-112, -80] + 0xff, + 0xb0, + opROUND00, // [-112, -48] + opPUSHW000, // [-112, -48, -17] + 0xff, + 0xef, + opROUND00, // [-112, -48, -48] + opPUSHW000, // [-112, -48, -48, -16] + 0xff, + 0xf0, + opROUND00, // [-112, -48, -48, -48] + opPUSHB000, // [-112, -48, -48, -48, 0] + 0, + opROUND00, // [-112, -48, -48, -48, 16] + opPUSHB000, // [-112, -48, -48, -48, 16, 16] + 16, + opROUND00, // [-112, -48, -48, -48, 16, 16] + opPUSHB000, // [-112, -48, -48, -48, 16, 16, 47] + 47, + opROUND00, // [-112, -48, -48, -48, 16, 16, 16] + opPUSHB000, // [-112, -48, -48, -48, 16, 16, 16, 48] + 48, + opROUND00, // [-112, -48, -48, -48, 16, 16, 16, 80] + }, + []int32{-112, -48, -48, -48, 16, 16, 16, 80}, + "", + }, } for _, tc := range testCases { diff --git a/freetype/truetype/opcodes.go b/freetype/truetype/opcodes.go index aa1dd54..ae49474 100644 --- a/freetype/truetype/opcodes.go +++ b/freetype/truetype/opcodes.go @@ -34,8 +34,8 @@ const ( opSZP2 = 0x15 opSZPS = 0x16 opSLOOP = 0x17 - opRTG = 0x18 - opRTHG = 0x19 + opRTG = 0x18 // Round To Grid + opRTHG = 0x19 // Round To Half Grid opSMD = 0x1a opELSE = 0x1b // ELSE clause opJMPR = 0x1c // JuMP Relative @@ -71,7 +71,7 @@ const ( opMSIRP0 = 0x3a opMSIRP1 = 0x3b opALIGNRP = 0x3c - opRTGD = 0x3d + opRTDG = 0x3d // Round To Double Grid opMIAP0 = 0x3e opMIAP1 = 0x3f opNPUSHB = 0x40 // PUSH N Bytes @@ -114,28 +114,28 @@ const ( opNEG = 0x65 // NEGate opFLOOR = 0x66 // FLOOR opCEILING = 0x67 // CEILING - opROUND00 = 0x68 - opROUND01 = 0x69 - opROUND10 = 0x6a - opROUND11 = 0x6b - opNROUND00 = 0x6c - opNROUND01 = 0x6d - opNROUND10 = 0x6e - opNROUND11 = 0x6f + opROUND00 = 0x68 // ROUND value + opROUND01 = 0x69 // . + opROUND10 = 0x6a // . + opROUND11 = 0x6b // . + opNROUND00 = 0x6c // No ROUNDing of value + opNROUND01 = 0x6d // . + opNROUND10 = 0x6e // . + opNROUND11 = 0x6f // . opWCVTF = 0x70 opDELTAP2 = 0x71 opDELTAP3 = 0x72 opDELTAC1 = 0x73 opDELTAC2 = 0x74 opDELTAC3 = 0x75 - opSROUND = 0x76 - opS45ROUND = 0x77 + opSROUND = 0x76 // Super ROUND + opS45ROUND = 0x77 // Super ROUND 45 degrees opJROT = 0x78 // Jump Relative On True opJROF = 0x79 // Jump Relative On False - opROFF = 0x7a + opROFF = 0x7a // Round OFF op_0x7b = 0x7b - opRUTG = 0x7c - opRDTG = 0x7d + opRUTG = 0x7c // Round Up To Grid + opRDTG = 0x7d // Round Down To Grid opSANGW = 0x7e opAA = 0x7f opFLIPPT = 0x80 @@ -272,13 +272,13 @@ const ( var popCount = [256]uint8{ // 1, 2, 3, 4, 5, 6, 7, 8, 9, a, b, c, d, e, f q, q, q, q, q, q, q, q, q, q, q, q, q, q, q, q, // 0x00 - 0x0f - q, q, q, q, q, q, q, q, q, q, q, 0, 1, q, q, q, // 0x10 - 0x1f + q, q, q, q, q, q, q, q, 0, 0, q, 0, 1, q, q, q, // 0x10 - 0x1f 1, 1, 0, 2, 0, 1, 1, q, q, q, q, q, q, q, q, q, // 0x20 - 0x2f - q, q, q, q, q, q, q, q, q, q, q, q, q, q, q, q, // 0x30 - 0x3f + q, q, q, q, q, q, q, q, q, q, q, q, q, 0, q, q, // 0x30 - 0x3f 0, 0, q, q, q, q, q, q, q, q, q, q, q, q, q, 0, // 0x40 - 0x4f 2, 2, 2, 2, 2, 2, q, q, 1, 0, 2, 2, 1, q, q, q, // 0x50 - 0x5f - 2, 2, 2, 2, 1, 1, 1, 1, q, q, q, q, q, q, q, q, // 0x60 - 0x6f - q, q, q, q, q, q, q, q, 2, 2, q, q, q, q, q, q, // 0x70 - 0x7f + 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0x60 - 0x6f + q, q, q, q, q, q, 1, 1, 2, 2, 0, q, 0, 0, q, q, // 0x70 - 0x7f q, q, q, q, q, q, q, q, q, q, q, q, q, q, q, q, // 0x80 - 0x8f 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