font/sfnt: parse Type 2 Charstrings.
Change-Id: I61beec4611612800a519045e2552c513eb83c3f8 Reviewed-on: https://go-review.googlesource.com/33932 Reviewed-by: Dave Day <djd@golang.org>
This commit is contained in:
parent
ae63d5d566
commit
ce50dba65c
|
@ -51,6 +51,8 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"golang.org/x/image/math/fixed"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -86,18 +88,7 @@ type cffParser struct {
|
||||||
buf []byte
|
buf []byte
|
||||||
locBuf [2]uint32
|
locBuf [2]uint32
|
||||||
|
|
||||||
parseNumberBuf [maxRealNumberStrLen]byte
|
psi psInterpreter
|
||||||
|
|
||||||
instructions []byte
|
|
||||||
|
|
||||||
stack struct {
|
|
||||||
a [psStackSize]int32
|
|
||||||
top int32
|
|
||||||
}
|
|
||||||
|
|
||||||
saved struct {
|
|
||||||
charStrings int32
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *cffParser) parse() (locations []uint32, err error) {
|
func (p *cffParser) parse() (locations []uint32, err error) {
|
||||||
|
@ -145,20 +136,17 @@ func (p *cffParser) parse() (locations []uint32, err error) {
|
||||||
if !p.read(int(p.locBuf[1] - p.locBuf[0])) {
|
if !p.read(int(p.locBuf[1] - p.locBuf[0])) {
|
||||||
return nil, p.err
|
return nil, p.err
|
||||||
}
|
}
|
||||||
|
p.psi.topDict.initialize()
|
||||||
for p.instructions = p.buf; len(p.instructions) > 0; {
|
if p.err = p.psi.run(psContextTopDict, p.buf); p.err != nil {
|
||||||
p.step()
|
|
||||||
if p.err != nil {
|
|
||||||
return nil, p.err
|
return nil, p.err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the CharStrings INDEX, whose location was found in the Top DICT.
|
// Parse the CharStrings INDEX, whose location was found in the Top DICT.
|
||||||
if p.saved.charStrings <= 0 || int32(p.end-p.base) < p.saved.charStrings {
|
if p.psi.topDict.charStrings <= 0 || int32(p.end-p.base) < p.psi.topDict.charStrings {
|
||||||
return nil, errInvalidCFFTable
|
return nil, errInvalidCFFTable
|
||||||
}
|
}
|
||||||
p.offset = p.base + int(p.saved.charStrings)
|
p.offset = p.base + int(p.psi.topDict.charStrings)
|
||||||
count, offSize, ok := p.parseIndexHeader()
|
count, offSize, ok := p.parseIndexHeader()
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, p.err
|
return nil, p.err
|
||||||
|
@ -265,99 +253,129 @@ func (p *cffParser) parseIndexLocations(dst []uint32, count, offSize int32) (ok
|
||||||
return p.err == nil
|
return p.err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// step executes a single operation, whether pushing a numeric operand onto the
|
type psContext uint32
|
||||||
// stack or executing an operator.
|
|
||||||
func (p *cffParser) step() {
|
const (
|
||||||
if number, res := p.parseNumber(); res != prNone {
|
psContextTopDict psContext = iota
|
||||||
if res < 0 || p.stack.top == psStackSize {
|
psContextType2Charstring
|
||||||
if res == prUnsupportedRNE {
|
)
|
||||||
p.err = errUnsupportedRealNumberEncoding
|
|
||||||
} else {
|
// psTopDictData contains fields specific to the Top DICT context.
|
||||||
p.err = errInvalidCFFTable
|
type psTopDictData struct {
|
||||||
|
charStrings int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *psTopDictData) initialize() {
|
||||||
|
*d = psTopDictData{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// psType2CharstringsData contains fields specific to the Type 2 Charstrings
|
||||||
|
// context.
|
||||||
|
type psType2CharstringsData struct {
|
||||||
|
segments []Segment
|
||||||
|
x, y int32
|
||||||
|
hintBits int32
|
||||||
|
seenWidth bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *psType2CharstringsData) initialize(segments []Segment) {
|
||||||
|
*d = psType2CharstringsData{
|
||||||
|
segments: segments,
|
||||||
}
|
}
|
||||||
return
|
}
|
||||||
|
|
||||||
|
// psInterpreter is a PostScript interpreter.
|
||||||
|
type psInterpreter struct {
|
||||||
|
ctx psContext
|
||||||
|
instructions []byte
|
||||||
|
stack struct {
|
||||||
|
a [psStackSize]int32
|
||||||
|
top int32
|
||||||
}
|
}
|
||||||
p.stack.a[p.stack.top] = number
|
parseNumberBuf [maxRealNumberStrLen]byte
|
||||||
p.stack.top++
|
topDict psTopDictData
|
||||||
return
|
type2Charstrings psType2CharstringsData
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *psInterpreter) run(ctx psContext, instructions []byte) error {
|
||||||
|
p.ctx = ctx
|
||||||
|
p.instructions = instructions
|
||||||
|
p.stack.top = 0
|
||||||
|
|
||||||
|
loop:
|
||||||
|
for len(p.instructions) > 0 {
|
||||||
|
// Push a numeric operand on the stack, if applicable.
|
||||||
|
if hasResult, err := p.parseNumber(); hasResult {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
b0 := p.instructions[0]
|
// Otherwise, execute an operator.
|
||||||
|
b := p.instructions[0]
|
||||||
p.instructions = p.instructions[1:]
|
p.instructions = p.instructions[1:]
|
||||||
|
|
||||||
for b, escaped, operators := b0, false, topDictOperators[0]; ; {
|
for escaped, ops := false, psOperators[ctx][0]; ; {
|
||||||
if b == escapeByte && !escaped {
|
if b == escapeByte && !escaped {
|
||||||
if len(p.instructions) <= 0 {
|
if len(p.instructions) <= 0 {
|
||||||
p.err = errInvalidCFFTable
|
return errInvalidCFFTable
|
||||||
return
|
|
||||||
}
|
}
|
||||||
b = p.instructions[0]
|
b = p.instructions[0]
|
||||||
p.instructions = p.instructions[1:]
|
p.instructions = p.instructions[1:]
|
||||||
escaped = true
|
escaped = true
|
||||||
operators = topDictOperators[1]
|
ops = psOperators[ctx][1]
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if int(b) < len(operators) {
|
if int(b) < len(ops) {
|
||||||
if op := operators[b]; op.name != "" {
|
if op := ops[b]; op.name != "" {
|
||||||
if p.stack.top < op.numPop {
|
if p.stack.top < op.numPop {
|
||||||
p.err = errInvalidCFFTable
|
return errInvalidCFFTable
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if op.run != nil {
|
if op.run != nil {
|
||||||
op.run(p)
|
if err := op.run(p); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if op.numPop < 0 {
|
if op.numPop < 0 {
|
||||||
p.stack.top = 0
|
p.stack.top = 0
|
||||||
} else {
|
} else {
|
||||||
p.stack.top -= op.numPop
|
p.stack.top -= op.numPop
|
||||||
}
|
}
|
||||||
return
|
continue loop
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if escaped {
|
if escaped {
|
||||||
p.err = fmt.Errorf("sfnt: unrecognized CFF 2-byte operator (12 %d)", b)
|
return fmt.Errorf("sfnt: unrecognized CFF 2-byte operator (12 %d)", b)
|
||||||
} else {
|
} else {
|
||||||
p.err = fmt.Errorf("sfnt: unrecognized CFF 1-byte operator (%d)", b)
|
return fmt.Errorf("sfnt: unrecognized CFF 1-byte operator (%d)", b)
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type parseResult int32
|
|
||||||
|
|
||||||
const (
|
|
||||||
prUnsupportedRNE parseResult = -2
|
|
||||||
prInvalid parseResult = -1
|
|
||||||
prNone parseResult = +0
|
|
||||||
prGood parseResult = +1
|
|
||||||
)
|
|
||||||
|
|
||||||
// See 5176.CFF.pdf section 4 "DICT Data".
|
// See 5176.CFF.pdf section 4 "DICT Data".
|
||||||
func (p *cffParser) parseNumber() (number int32, res parseResult) {
|
func (p *psInterpreter) parseNumber() (hasResult bool, err error) {
|
||||||
if len(p.instructions) == 0 {
|
number := int32(0)
|
||||||
return 0, prNone
|
switch b := p.instructions[0]; {
|
||||||
}
|
case b == 28:
|
||||||
|
|
||||||
switch b0 := p.instructions[0]; {
|
|
||||||
case b0 == 28:
|
|
||||||
if len(p.instructions) < 3 {
|
if len(p.instructions) < 3 {
|
||||||
return 0, prInvalid
|
return true, errInvalidCFFTable
|
||||||
}
|
}
|
||||||
number = int32(int16(u16(p.instructions[1:])))
|
number, hasResult = int32(int16(u16(p.instructions[1:]))), true
|
||||||
p.instructions = p.instructions[3:]
|
p.instructions = p.instructions[3:]
|
||||||
return number, prGood
|
|
||||||
|
|
||||||
case b0 == 29:
|
case b == 29 && p.ctx == psContextTopDict:
|
||||||
if len(p.instructions) < 5 {
|
if len(p.instructions) < 5 {
|
||||||
return 0, prInvalid
|
return true, errInvalidCFFTable
|
||||||
}
|
}
|
||||||
number = int32(u32(p.instructions[1:]))
|
number, hasResult = int32(u32(p.instructions[1:])), true
|
||||||
p.instructions = p.instructions[5:]
|
p.instructions = p.instructions[5:]
|
||||||
return number, prGood
|
|
||||||
|
|
||||||
case b0 == 30:
|
case b == 30 && p.ctx == psContextTopDict:
|
||||||
// Parse a real number. This isn't listed in 5176.CFF.pdf Table 3
|
// Parse a real number. This isn't listed in 5176.CFF.pdf Table 3
|
||||||
// "Operand Encoding" but that table lists integer encodings. Further
|
// "Operand Encoding" but that table lists integer encodings. Further
|
||||||
// down the page it says "A real number operand is provided in addition
|
// down the page it says "A real number operand is provided in addition
|
||||||
|
@ -366,9 +384,10 @@ func (p *cffParser) parseNumber() (number int32, res parseResult) {
|
||||||
|
|
||||||
s := p.parseNumberBuf[:0]
|
s := p.parseNumberBuf[:0]
|
||||||
p.instructions = p.instructions[1:]
|
p.instructions = p.instructions[1:]
|
||||||
|
loop:
|
||||||
for {
|
for {
|
||||||
if len(p.instructions) == 0 {
|
if len(p.instructions) == 0 {
|
||||||
return 0, prInvalid
|
return true, errInvalidCFFTable
|
||||||
}
|
}
|
||||||
b := p.instructions[0]
|
b := p.instructions[0]
|
||||||
p.instructions = p.instructions[1:]
|
p.instructions = p.instructions[1:]
|
||||||
|
@ -379,45 +398,60 @@ func (p *cffParser) parseNumber() (number int32, res parseResult) {
|
||||||
if nib == 0x0f {
|
if nib == 0x0f {
|
||||||
f, err := strconv.ParseFloat(string(s), 32)
|
f, err := strconv.ParseFloat(string(s), 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, prInvalid
|
return true, errInvalidCFFTable
|
||||||
}
|
}
|
||||||
return int32(math.Float32bits(float32(f))), prGood
|
number, hasResult = int32(math.Float32bits(float32(f))), true
|
||||||
|
break loop
|
||||||
}
|
}
|
||||||
if nib == 0x0d {
|
if nib == 0x0d {
|
||||||
return 0, prInvalid
|
return true, errInvalidCFFTable
|
||||||
}
|
}
|
||||||
if len(s)+maxNibbleDefsLength > len(p.parseNumberBuf) {
|
if len(s)+maxNibbleDefsLength > len(p.parseNumberBuf) {
|
||||||
return 0, prUnsupportedRNE
|
return true, errUnsupportedRealNumberEncoding
|
||||||
}
|
}
|
||||||
s = append(s, nibbleDefs[nib]...)
|
s = append(s, nibbleDefs[nib]...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case b0 < 32:
|
case b < 32:
|
||||||
// No-op.
|
// No-op.
|
||||||
|
|
||||||
case b0 < 247:
|
case b < 247:
|
||||||
p.instructions = p.instructions[1:]
|
p.instructions = p.instructions[1:]
|
||||||
return int32(b0) - 139, prGood
|
number, hasResult = int32(b)-139, true
|
||||||
|
|
||||||
case b0 < 251:
|
case b < 251:
|
||||||
if len(p.instructions) < 2 {
|
if len(p.instructions) < 2 {
|
||||||
return 0, prInvalid
|
return true, errInvalidCFFTable
|
||||||
}
|
}
|
||||||
b1 := p.instructions[1]
|
b1 := p.instructions[1]
|
||||||
p.instructions = p.instructions[2:]
|
p.instructions = p.instructions[2:]
|
||||||
return +int32(b0-247)*256 + int32(b1) + 108, prGood
|
number, hasResult = +int32(b-247)*256+int32(b1)+108, true
|
||||||
|
|
||||||
case b0 < 255:
|
case b < 255:
|
||||||
if len(p.instructions) < 2 {
|
if len(p.instructions) < 2 {
|
||||||
return 0, prInvalid
|
return true, errInvalidCFFTable
|
||||||
}
|
}
|
||||||
b1 := p.instructions[1]
|
b1 := p.instructions[1]
|
||||||
p.instructions = p.instructions[2:]
|
p.instructions = p.instructions[2:]
|
||||||
return -int32(b0-251)*256 - int32(b1) - 108, prGood
|
number, hasResult = -int32(b-251)*256-int32(b1)-108, true
|
||||||
|
|
||||||
|
case b == 255 && p.ctx == psContextType2Charstring:
|
||||||
|
if len(p.instructions) < 5 {
|
||||||
|
return true, errInvalidCFFTable
|
||||||
|
}
|
||||||
|
number, hasResult = int32(u32(p.instructions[1:])), true
|
||||||
|
p.instructions = p.instructions[5:]
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0, prNone
|
if hasResult {
|
||||||
|
if p.stack.top == psStackSize {
|
||||||
|
return true, errInvalidCFFTable
|
||||||
|
}
|
||||||
|
p.stack.a[p.stack.top] = number
|
||||||
|
p.stack.top++
|
||||||
|
}
|
||||||
|
return hasResult, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxNibbleDefsLength = len("E-")
|
const maxNibbleDefsLength = len("E-")
|
||||||
|
@ -442,7 +476,7 @@ var nibbleDefs = [16]string{
|
||||||
0x0f: "",
|
0x0f: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
type cffOperator struct {
|
type psOperator struct {
|
||||||
// numPop is the number of stack values to pop. -1 means "array" and -2
|
// numPop is the number of stack values to pop. -1 means "array" and -2
|
||||||
// means "delta" as per 5176.CFF.pdf Table 6 "Operand Types".
|
// means "delta" as per 5176.CFF.pdf Table 6 "Operand Types".
|
||||||
numPop int32
|
numPop int32
|
||||||
|
@ -451,13 +485,15 @@ type cffOperator struct {
|
||||||
name string
|
name string
|
||||||
// run is the function that implements the operator. Nil means that we
|
// run is the function that implements the operator. Nil means that we
|
||||||
// ignore the operator, other than popping its arguments off the stack.
|
// ignore the operator, other than popping its arguments off the stack.
|
||||||
run func(*cffParser)
|
run func(*psInterpreter) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// topDictOperators encodes the subset of 5176.CFF.pdf Table 9 "Top DICT
|
// psOperators holds the 1-byte and 2-byte operators for PostScript interpreter
|
||||||
// Operator Entries" and Table 10 "CIDFont Operator Extensions" used by this
|
// contexts.
|
||||||
// implementation.
|
var psOperators = [...][2][]psOperator{
|
||||||
var topDictOperators = [2][]cffOperator{{
|
// The Top DICT operators are defined by 5176.CFF.pdf Table 9 "Top DICT
|
||||||
|
// Operator Entries" and Table 10 "CIDFont Operator Extensions".
|
||||||
|
psContextTopDict: {{
|
||||||
// 1-byte operators.
|
// 1-byte operators.
|
||||||
0: {+1, "version", nil},
|
0: {+1, "version", nil},
|
||||||
1: {+1, "Notice", nil},
|
1: {+1, "Notice", nil},
|
||||||
|
@ -469,11 +505,12 @@ var topDictOperators = [2][]cffOperator{{
|
||||||
14: {-1, "XUID", nil},
|
14: {-1, "XUID", nil},
|
||||||
15: {+1, "charset", nil},
|
15: {+1, "charset", nil},
|
||||||
16: {+1, "Encoding", nil},
|
16: {+1, "Encoding", nil},
|
||||||
17: {+1, "CharStrings", func(p *cffParser) {
|
17: {+1, "CharStrings", func(p *psInterpreter) error {
|
||||||
p.saved.charStrings = p.stack.a[p.stack.top-1]
|
p.topDict.charStrings = p.stack.a[p.stack.top-1]
|
||||||
|
return nil
|
||||||
}},
|
}},
|
||||||
18: {+2, "Private", nil},
|
18: {+2, "Private", nil},
|
||||||
}, {
|
}, {
|
||||||
// 2-byte operators. The first byte is the escape byte.
|
// 2-byte operators. The first byte is the escape byte.
|
||||||
0: {+1, "Copyright", nil},
|
0: {+1, "Copyright", nil},
|
||||||
1: {+1, "isFixedPitch", nil},
|
1: {+1, "isFixedPitch", nil},
|
||||||
|
@ -497,8 +534,315 @@ var topDictOperators = [2][]cffOperator{{
|
||||||
36: {+1, "FDArray", nil},
|
36: {+1, "FDArray", nil},
|
||||||
37: {+1, "FDSelect", nil},
|
37: {+1, "FDSelect", nil},
|
||||||
38: {+1, "FontName", nil},
|
38: {+1, "FontName", nil},
|
||||||
}}
|
}},
|
||||||
|
|
||||||
|
// The Type 2 Charstring operators are defined by 5177.Type2.pdf Appendix A
|
||||||
|
// "Type 2 Charstring Command Codes".
|
||||||
|
psContextType2Charstring: {{
|
||||||
|
// 1-byte operators.
|
||||||
|
0: {}, // Reserved.
|
||||||
|
1: {-1, "hstem", t2CStem},
|
||||||
|
2: {}, // Reserved.
|
||||||
|
3: {-1, "vstem", t2CStem},
|
||||||
|
4: {-1, "vmoveto", t2CVmoveto},
|
||||||
|
5: {-1, "rlineto", t2CRlineto},
|
||||||
|
6: {-1, "hlineto", t2CHlineto},
|
||||||
|
7: {-1, "vlineto", t2CVlineto},
|
||||||
|
8: {}, // rrcurveto.
|
||||||
|
9: {}, // Reserved.
|
||||||
|
10: {}, // callsubr.
|
||||||
|
11: {}, // return.
|
||||||
|
12: {}, // escape.
|
||||||
|
13: {}, // Reserved.
|
||||||
|
14: {-1, "endchar", t2CEndchar},
|
||||||
|
15: {}, // Reserved.
|
||||||
|
16: {}, // Reserved.
|
||||||
|
17: {}, // Reserved.
|
||||||
|
18: {-1, "hstemhm", t2CStem},
|
||||||
|
19: {-1, "hintmask", t2CMask},
|
||||||
|
20: {-1, "cntrmask", t2CMask},
|
||||||
|
21: {-1, "rmoveto", t2CRmoveto},
|
||||||
|
22: {-1, "hmoveto", t2CHmoveto},
|
||||||
|
23: {-1, "vstemhm", t2CStem},
|
||||||
|
24: {}, // rcurveline.
|
||||||
|
25: {}, // rlinecurve.
|
||||||
|
26: {}, // vvcurveto.
|
||||||
|
27: {}, // hhcurveto.
|
||||||
|
28: {}, // shortint.
|
||||||
|
29: {}, // callgsubr.
|
||||||
|
30: {-1, "vhcurveto", t2CVhcurveto},
|
||||||
|
31: {-1, "hvcurveto", t2CHvcurveto},
|
||||||
|
}, {
|
||||||
|
// 2-byte operators. The first byte is the escape byte.
|
||||||
|
0: {}, // Reserved.
|
||||||
|
// TODO: more operators.
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
// 5176.CFF.pdf section 4 "DICT Data" says that "Two-byte operators have an
|
// 5176.CFF.pdf section 4 "DICT Data" says that "Two-byte operators have an
|
||||||
// initial escape byte of 12".
|
// initial escape byte of 12".
|
||||||
const escapeByte = 12
|
const escapeByte = 12
|
||||||
|
|
||||||
|
// t2CReadWidth reads the optional width adjustment. If present, it is on the
|
||||||
|
// bottom of the stack.
|
||||||
|
//
|
||||||
|
// 5177.Type2.pdf page 16 Note 4 says: "The first stack-clearing operator,
|
||||||
|
// which must be one of hstem, hstemhm, vstem, vstemhm, cntrmask, hintmask,
|
||||||
|
// hmoveto, vmoveto, rmoveto, or endchar, takes an additional argument — the
|
||||||
|
// width... which may be expressed as zero or one numeric argument."
|
||||||
|
func t2CReadWidth(p *psInterpreter, nArgs int32) {
|
||||||
|
if p.type2Charstrings.seenWidth {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.type2Charstrings.seenWidth = true
|
||||||
|
switch nArgs {
|
||||||
|
case 0:
|
||||||
|
if p.stack.top != 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case 1:
|
||||||
|
if p.stack.top <= 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if p.stack.top%nArgs != 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// When parsing a standalone CFF, we'd save the value of p.stack.a[0] here
|
||||||
|
// as it defines the glyph's width (horizontal advance). Specifically, if
|
||||||
|
// present, it is a delta to the font-global nominalWidthX value found in
|
||||||
|
// the Private DICT. If absent, the glyph's width is the defaultWidthX
|
||||||
|
// value in that dict. See 5176.CFF.pdf section 15 "Private DICT Data".
|
||||||
|
//
|
||||||
|
// For a CFF embedded in an SFNT font (i.e. an OpenType font), glyph widths
|
||||||
|
// are already stored in the hmtx table, separate to the CFF table, and it
|
||||||
|
// is simpler to parse that table for all OpenType fonts (PostScript and
|
||||||
|
// TrueType). We therefore ignore the width value here, and just remove it
|
||||||
|
// from the bottom of the stack.
|
||||||
|
copy(p.stack.a[:p.stack.top-1], p.stack.a[1:p.stack.top])
|
||||||
|
p.stack.top--
|
||||||
|
}
|
||||||
|
|
||||||
|
func t2CStem(p *psInterpreter) error {
|
||||||
|
t2CReadWidth(p, 2)
|
||||||
|
if p.stack.top%2 != 0 {
|
||||||
|
return errInvalidCFFTable
|
||||||
|
}
|
||||||
|
// We update the number of hintBits need to parse hintmask and cntrmask
|
||||||
|
// instructions, but this Type 2 Charstring implementation otherwise
|
||||||
|
// ignores the stem hints.
|
||||||
|
p.type2Charstrings.hintBits += p.stack.top / 2
|
||||||
|
if p.type2Charstrings.hintBits > maxHintBits {
|
||||||
|
return errUnsupportedNumberOfHints
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func t2CMask(p *psInterpreter) error {
|
||||||
|
hintBytes := (p.type2Charstrings.hintBits + 7) / 8
|
||||||
|
t2CReadWidth(p, hintBytes)
|
||||||
|
if len(p.instructions) < int(hintBytes) {
|
||||||
|
return errInvalidCFFTable
|
||||||
|
}
|
||||||
|
p.instructions = p.instructions[hintBytes:]
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func t2CAppendMoveto(p *psInterpreter) {
|
||||||
|
p.type2Charstrings.segments = append(p.type2Charstrings.segments, Segment{
|
||||||
|
Op: SegmentOpMoveTo,
|
||||||
|
Args: [6]fixed.Int26_6{
|
||||||
|
0: fixed.Int26_6(p.type2Charstrings.x) << 6,
|
||||||
|
1: fixed.Int26_6(p.type2Charstrings.y) << 6,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func t2CAppendLineto(p *psInterpreter) {
|
||||||
|
p.type2Charstrings.segments = append(p.type2Charstrings.segments, Segment{
|
||||||
|
Op: SegmentOpLineTo,
|
||||||
|
Args: [6]fixed.Int26_6{
|
||||||
|
0: fixed.Int26_6(p.type2Charstrings.x) << 6,
|
||||||
|
1: fixed.Int26_6(p.type2Charstrings.y) << 6,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func t2CAppendCubeto(p *psInterpreter, dxa, dya, dxb, dyb, dxc, dyc int32) {
|
||||||
|
p.type2Charstrings.x += dxa
|
||||||
|
p.type2Charstrings.y += dya
|
||||||
|
xa := p.type2Charstrings.x
|
||||||
|
ya := p.type2Charstrings.y
|
||||||
|
p.type2Charstrings.x += dxb
|
||||||
|
p.type2Charstrings.y += dyb
|
||||||
|
xb := p.type2Charstrings.x
|
||||||
|
yb := p.type2Charstrings.y
|
||||||
|
p.type2Charstrings.x += dxc
|
||||||
|
p.type2Charstrings.y += dyc
|
||||||
|
xc := p.type2Charstrings.x
|
||||||
|
yc := p.type2Charstrings.y
|
||||||
|
p.type2Charstrings.segments = append(p.type2Charstrings.segments, Segment{
|
||||||
|
Op: SegmentOpCubeTo,
|
||||||
|
Args: [6]fixed.Int26_6{
|
||||||
|
0: fixed.Int26_6(xa) << 6,
|
||||||
|
1: fixed.Int26_6(ya) << 6,
|
||||||
|
2: fixed.Int26_6(xb) << 6,
|
||||||
|
3: fixed.Int26_6(yb) << 6,
|
||||||
|
4: fixed.Int26_6(xc) << 6,
|
||||||
|
5: fixed.Int26_6(yc) << 6,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func t2CHmoveto(p *psInterpreter) error {
|
||||||
|
t2CReadWidth(p, 1)
|
||||||
|
if p.stack.top < 1 {
|
||||||
|
return errInvalidCFFTable
|
||||||
|
}
|
||||||
|
for i := int32(0); i < p.stack.top; i++ {
|
||||||
|
p.type2Charstrings.x += p.stack.a[i]
|
||||||
|
}
|
||||||
|
t2CAppendMoveto(p)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func t2CVmoveto(p *psInterpreter) error {
|
||||||
|
t2CReadWidth(p, 1)
|
||||||
|
if p.stack.top < 1 {
|
||||||
|
return errInvalidCFFTable
|
||||||
|
}
|
||||||
|
for i := int32(0); i < p.stack.top; i++ {
|
||||||
|
p.type2Charstrings.y += p.stack.a[i]
|
||||||
|
}
|
||||||
|
t2CAppendMoveto(p)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func t2CRmoveto(p *psInterpreter) error {
|
||||||
|
t2CReadWidth(p, 2)
|
||||||
|
if p.stack.top < 2 || p.stack.top%2 != 0 {
|
||||||
|
return errInvalidCFFTable
|
||||||
|
}
|
||||||
|
for i := int32(0); i < p.stack.top; i += 2 {
|
||||||
|
p.type2Charstrings.x += p.stack.a[i+0]
|
||||||
|
p.type2Charstrings.y += p.stack.a[i+1]
|
||||||
|
}
|
||||||
|
t2CAppendMoveto(p)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func t2CHlineto(p *psInterpreter) error { return t2CLineto(p, false) }
|
||||||
|
func t2CVlineto(p *psInterpreter) error { return t2CLineto(p, true) }
|
||||||
|
|
||||||
|
func t2CLineto(p *psInterpreter, vertical bool) error {
|
||||||
|
if !p.type2Charstrings.seenWidth {
|
||||||
|
return errInvalidCFFTable
|
||||||
|
}
|
||||||
|
if p.stack.top < 1 {
|
||||||
|
return errInvalidCFFTable
|
||||||
|
}
|
||||||
|
for i := int32(0); i < p.stack.top; i, vertical = i+1, !vertical {
|
||||||
|
if vertical {
|
||||||
|
p.type2Charstrings.y += p.stack.a[i]
|
||||||
|
} else {
|
||||||
|
p.type2Charstrings.x += p.stack.a[i]
|
||||||
|
}
|
||||||
|
t2CAppendLineto(p)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func t2CRlineto(p *psInterpreter) error {
|
||||||
|
if !p.type2Charstrings.seenWidth {
|
||||||
|
return errInvalidCFFTable
|
||||||
|
}
|
||||||
|
if p.stack.top < 2 || p.stack.top%2 != 0 {
|
||||||
|
return errInvalidCFFTable
|
||||||
|
}
|
||||||
|
for i := int32(0); i < p.stack.top; i += 2 {
|
||||||
|
p.type2Charstrings.x += p.stack.a[i+0]
|
||||||
|
p.type2Charstrings.y += p.stack.a[i+1]
|
||||||
|
t2CAppendLineto(p)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// As per 5177.Type2.pdf section 4.1 "Path Construction Operators",
|
||||||
|
//
|
||||||
|
// hvcurveto is one of:
|
||||||
|
// - dx1 dx2 dy2 dy3 {dya dxb dyb dxc dxd dxe dye dyf}* dxf?
|
||||||
|
// - {dxa dxb dyb dyc dyd dxe dye dxf}+ dyf?
|
||||||
|
//
|
||||||
|
// vhcurveto is one of:
|
||||||
|
// - dy1 dx2 dy2 dx3 {dxa dxb dyb dyc dyd dxe dye dxf}* dyf?
|
||||||
|
// - {dya dxb dyb dxc dxd dxe dye dyf}+ dxf?
|
||||||
|
|
||||||
|
func t2CHvcurveto(p *psInterpreter) error { return t2CCurveto(p, false) }
|
||||||
|
func t2CVhcurveto(p *psInterpreter) error { return t2CCurveto(p, true) }
|
||||||
|
|
||||||
|
func t2CCurveto(p *psInterpreter, vertical bool) error {
|
||||||
|
if !p.type2Charstrings.seenWidth || p.stack.top < 4 {
|
||||||
|
return errInvalidCFFTable
|
||||||
|
}
|
||||||
|
for i := int32(0); i != p.stack.top; vertical = !vertical {
|
||||||
|
if vertical {
|
||||||
|
i = t2CVcurveto(p, i)
|
||||||
|
} else {
|
||||||
|
i = t2CHcurveto(p, i)
|
||||||
|
}
|
||||||
|
if i < 0 {
|
||||||
|
return errInvalidCFFTable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func t2CHcurveto(p *psInterpreter, i int32) (j int32) {
|
||||||
|
if i+4 > p.stack.top {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
dxa := p.stack.a[i+0]
|
||||||
|
dxb := p.stack.a[i+1]
|
||||||
|
dyb := p.stack.a[i+2]
|
||||||
|
dyc := p.stack.a[i+3]
|
||||||
|
dxc := int32(0)
|
||||||
|
i += 4
|
||||||
|
if i+1 == p.stack.top {
|
||||||
|
dxc = p.stack.a[i]
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
t2CAppendCubeto(p, dxa, 0, dxb, dyb, dxc, dyc)
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
func t2CVcurveto(p *psInterpreter, i int32) (j int32) {
|
||||||
|
if i+4 > p.stack.top {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
dya := p.stack.a[i+0]
|
||||||
|
dxb := p.stack.a[i+1]
|
||||||
|
dyb := p.stack.a[i+2]
|
||||||
|
dxc := p.stack.a[i+3]
|
||||||
|
dyc := int32(0)
|
||||||
|
i += 4
|
||||||
|
if i+1 == p.stack.top {
|
||||||
|
dyc = p.stack.a[i]
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
t2CAppendCubeto(p, 0, dya, dxb, dyb, dxc, dyc)
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
func t2CEndchar(p *psInterpreter) error {
|
||||||
|
t2CReadWidth(p, 0)
|
||||||
|
if p.stack.top != 0 || len(p.instructions) != 0 {
|
||||||
|
if p.stack.top == 4 {
|
||||||
|
// TODO: process the implicit "seac" command as per 5177.Type2.pdf
|
||||||
|
// Appendix C "Compatibility and Deprecated Operators".
|
||||||
|
return errUnsupportedType2Charstring
|
||||||
|
}
|
||||||
|
return errInvalidCFFTable
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -17,11 +17,14 @@ package sfnt // import "golang.org/x/image/font/sfnt"
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
|
"golang.org/x/image/math/fixed"
|
||||||
)
|
)
|
||||||
|
|
||||||
// These constants are not part of the specifications, but are limitations used
|
// These constants are not part of the specifications, but are limitations used
|
||||||
// by this implementation.
|
// by this implementation.
|
||||||
const (
|
const (
|
||||||
|
maxHintBits = 256
|
||||||
maxNumTables = 256
|
maxNumTables = 256
|
||||||
maxRealNumberStrLen = 64 // Maximum length in bytes of the "-123.456E-7" representation.
|
maxRealNumberStrLen = 64 // Maximum length in bytes of the "-123.456E-7" representation.
|
||||||
|
|
||||||
|
@ -45,10 +48,15 @@ var (
|
||||||
|
|
||||||
errUnsupportedCFFVersion = errors.New("sfnt: unsupported CFF version")
|
errUnsupportedCFFVersion = errors.New("sfnt: unsupported CFF version")
|
||||||
errUnsupportedRealNumberEncoding = errors.New("sfnt: unsupported real number encoding")
|
errUnsupportedRealNumberEncoding = errors.New("sfnt: unsupported real number encoding")
|
||||||
|
errUnsupportedNumberOfHints = errors.New("sfnt: unsupported number of hints")
|
||||||
errUnsupportedNumberOfTables = errors.New("sfnt: unsupported number of tables")
|
errUnsupportedNumberOfTables = errors.New("sfnt: unsupported number of tables")
|
||||||
errUnsupportedTableOffsetLength = errors.New("sfnt: unsupported table offset or length")
|
errUnsupportedTableOffsetLength = errors.New("sfnt: unsupported table offset or length")
|
||||||
|
errUnsupportedType2Charstring = errors.New("sfnt: unsupported Type 2 Charstring")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// GlyphIndex is a glyph index in a Font.
|
||||||
|
type GlyphIndex uint16
|
||||||
|
|
||||||
// Units are an integral number of abstract, scalable "font units". The em
|
// Units are an integral number of abstract, scalable "font units". The em
|
||||||
// square is typically 1000 or 2048 "font units". This would map to a certain
|
// square is typically 1000 or 2048 "font units". This would map to a certain
|
||||||
// number (e.g. 30 pixels) of physical pixels, depending on things like the
|
// number (e.g. 30 pixels) of physical pixels, depending on things like the
|
||||||
|
@ -85,6 +93,16 @@ func (s *source) valid() bool {
|
||||||
return (s.b == nil) != (s.r == nil)
|
return (s.b == nil) != (s.r == nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// viewBufferWritable returns whether the []byte returned by source.view can be
|
||||||
|
// written to by the caller, including by passing it to the same method
|
||||||
|
// (source.view) on other receivers (i.e. different sources).
|
||||||
|
//
|
||||||
|
// In other words, it returns whether the source's underlying data is an
|
||||||
|
// io.ReaderAt, not a []byte.
|
||||||
|
func (s *source) viewBufferWritable() bool {
|
||||||
|
return s.b == nil
|
||||||
|
}
|
||||||
|
|
||||||
// view returns the length bytes at the given offset. buf is an optional
|
// view returns the length bytes at the given offset. buf is an optional
|
||||||
// scratch buffer to reduce allocations when calling view multiple times. A nil
|
// scratch buffer to reduce allocations when calling view multiple times. A nil
|
||||||
// buf is valid. The []byte returned may be a sub-slice of buf[:cap(buf)], or
|
// buf is valid. The []byte returned may be a sub-slice of buf[:cap(buf)], or
|
||||||
|
@ -159,6 +177,11 @@ func ParseReaderAt(src io.ReaderAt) (*Font, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Font is an SFNT font.
|
// Font is an SFNT font.
|
||||||
|
//
|
||||||
|
// All of its methods are safe to call concurrently, although the method
|
||||||
|
// arguments may have further restrictions. For example, it is valid to have
|
||||||
|
// multiple concurrent Font.LoadGlyph calls to the same *Font receiver, as long
|
||||||
|
// as each call has a different Buffer.
|
||||||
type Font struct {
|
type Font struct {
|
||||||
src source
|
src source
|
||||||
|
|
||||||
|
@ -343,11 +366,78 @@ func (f *Font) initialize() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Font) viewGlyphData(buf []byte, glyphIndex int) ([]byte, error) {
|
// TODO: func (f *Font) GlyphIndex(r rune) (x GlyphIndex, ok bool)
|
||||||
if glyphIndex < 0 || f.NumGlyphs() <= glyphIndex {
|
// This will require parsing the cmap table.
|
||||||
|
|
||||||
|
func (f *Font) viewGlyphData(buf []byte, x GlyphIndex) ([]byte, error) {
|
||||||
|
xx := int(x)
|
||||||
|
if f.NumGlyphs() <= xx {
|
||||||
return nil, errGlyphIndexOutOfRange
|
return nil, errGlyphIndexOutOfRange
|
||||||
}
|
}
|
||||||
i := f.cached.locations[glyphIndex+0]
|
i := f.cached.locations[xx+0]
|
||||||
j := f.cached.locations[glyphIndex+1]
|
j := f.cached.locations[xx+1]
|
||||||
return f.src.view(buf, int(i), int(j-i))
|
return f.src.view(buf, int(i), int(j-i))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LoadGlyphOptions are the options to the Font.LoadGlyph method.
|
||||||
|
type LoadGlyphOptions struct {
|
||||||
|
// TODO: scale / transform / hinting.
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadGlyph loads the glyphs vectors for the x'th glyph into b.Segments.
|
||||||
|
func (f *Font) LoadGlyph(b *Buffer, x GlyphIndex, opts *LoadGlyphOptions) error {
|
||||||
|
buf, err := f.viewGlyphData(b.buf, x)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Only update b.buf if it is safe to re-use buf.
|
||||||
|
if f.src.viewBufferWritable() {
|
||||||
|
b.buf = buf
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Segments = b.Segments[:0]
|
||||||
|
if f.cached.isPostScript {
|
||||||
|
b.psi.type2Charstrings.initialize(b.Segments)
|
||||||
|
if err := b.psi.run(psContextType2Charstring, buf); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
b.Segments = b.psi.type2Charstrings.segments
|
||||||
|
} else {
|
||||||
|
return errors.New("sfnt: TODO: load glyf data")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: look at opts to scale / transform / hint the Buffer.Segments.
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer holds the result of the Font.LoadGlyph method. It is valid to re-use
|
||||||
|
// a Buffer with multiple Font.LoadGlyph calls, even with different *Font
|
||||||
|
// receivers, as long as they are not concurrent calls.
|
||||||
|
//
|
||||||
|
// It is also valid to have multiple concurrent Font.LoadGlyph calls to the
|
||||||
|
// same *Font receiver, as long as each call has a different Buffer.
|
||||||
|
type Buffer struct {
|
||||||
|
Segments []Segment
|
||||||
|
// buf is a byte buffer for when a Font's source is an io.ReaderAt.
|
||||||
|
buf []byte
|
||||||
|
// psi is a PostScript interpreter for when the Font is an OpenType/CFF
|
||||||
|
// font.
|
||||||
|
psi psInterpreter
|
||||||
|
}
|
||||||
|
|
||||||
|
// Segment is a segment of a vector path.
|
||||||
|
type Segment struct {
|
||||||
|
Op SegmentOp
|
||||||
|
Args [6]fixed.Int26_6
|
||||||
|
}
|
||||||
|
|
||||||
|
// SegmentOp is a vector path segment's operator.
|
||||||
|
type SegmentOp uint32
|
||||||
|
|
||||||
|
const (
|
||||||
|
SegmentOpMoveTo SegmentOp = iota
|
||||||
|
SegmentOpLineTo
|
||||||
|
SegmentOpQuadTo
|
||||||
|
SegmentOpCubeTo
|
||||||
|
)
|
||||||
|
|
|
@ -11,8 +11,43 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"golang.org/x/image/font/gofont/goregular"
|
"golang.org/x/image/font/gofont/goregular"
|
||||||
|
"golang.org/x/image/math/fixed"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func moveTo(xa, ya int) Segment {
|
||||||
|
return Segment{
|
||||||
|
Op: SegmentOpMoveTo,
|
||||||
|
Args: [6]fixed.Int26_6{
|
||||||
|
0: fixed.I(xa),
|
||||||
|
1: fixed.I(ya),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func lineTo(xa, ya int) Segment {
|
||||||
|
return Segment{
|
||||||
|
Op: SegmentOpLineTo,
|
||||||
|
Args: [6]fixed.Int26_6{
|
||||||
|
0: fixed.I(xa),
|
||||||
|
1: fixed.I(ya),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cubeTo(xa, ya, xb, yb, xc, yc int) Segment {
|
||||||
|
return Segment{
|
||||||
|
Op: SegmentOpCubeTo,
|
||||||
|
Args: [6]fixed.Int26_6{
|
||||||
|
0: fixed.I(xa),
|
||||||
|
1: fixed.I(ya),
|
||||||
|
2: fixed.I(xb),
|
||||||
|
3: fixed.I(yb),
|
||||||
|
4: fixed.I(xc),
|
||||||
|
5: fixed.I(yc),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestTrueTypeParse(t *testing.T) {
|
func TestTrueTypeParse(t *testing.T) {
|
||||||
f, err := Parse(goregular.TTF)
|
f, err := Parse(goregular.TTF)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -51,28 +86,83 @@ func TestPostScript(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: replace this by a higher level test, once we parse Type 2
|
// wants' vectors correspond 1-to-1 to what's in the CFFTest.sfd file,
|
||||||
// Charstrings.
|
// although for some unknown reason, FontForge reverses the order somewhere
|
||||||
|
// along the way when converting from SFD to OpenType/CFF.
|
||||||
//
|
//
|
||||||
// As a sanity check for now, note that each string ends in '\x0e', which
|
// The .notdef glyph isn't explicitly in the SFD file, but for some unknown
|
||||||
// 5177.Type2.pdf Appendix A defines as "endchar".
|
// reason, FontForge generates a .notdef glyph in the OpenType/CFF file.
|
||||||
wants := [...]string{
|
wants := [...][]Segment{{
|
||||||
"\xf7\x63\x8b\xbd\xf8\x45\xbd\x01\xbd\xbd\xf7\xc0\xbd\x03\xbd\x16\xf8\x24\xf8\xa9\xfc\x24\x06\xbd\xfc\x77\x15\xf8\x45\xf7\xc0\xfc\x45\x07\x0e",
|
// .notdef
|
||||||
"\x8b\xef\xf8\xec\xef\x01\xef\xdb\xf7\x84\xdb\x03\xf7\xc0\xf9\x50\x15\xdb\xb3\xfb\x0c\x3b\xfb\x2a\x6d\xfb\x8e\x31\x3b\x63\xf7\x0c\xdb\xf7\x2a\xa9\xf7\x8e\xe5\x1f\xef\x04\x27\x27\xfb\x70\xfb\x48\xfb\x48\xef\xfb\x70\xef\xef\xef\xf7\x70\xf7\x48\xf7\x48\x27\xf7\x70\x27\x1f\x0e",
|
// - contour #0
|
||||||
"\xf6\xa0\x76\x01\xef\xf7\x5c\x03\xef\x16\xf7\x5c\xf9\xb4\xfb\x5c\x06\x0e",
|
moveTo(50, 0),
|
||||||
"\xf7\x89\xe1\x03\xf7\x21\xf8\x9c\x15\x87\xfb\x38\xf7\x00\xb7\xe1\xfc\x0a\xa3\xf8\x18\xf7\x00\x9f\x81\xf7\x4e\xfb\x04\x6f\x81\xf7\x3a\x33\x85\x83\xfb\x52\x05\x0e",
|
lineTo(450, 0),
|
||||||
}
|
lineTo(450, 533),
|
||||||
|
lineTo(50, 533),
|
||||||
|
// - contour #1
|
||||||
|
moveTo(100, 50),
|
||||||
|
lineTo(100, 483),
|
||||||
|
lineTo(400, 483),
|
||||||
|
lineTo(400, 50),
|
||||||
|
}, {
|
||||||
|
// zero
|
||||||
|
// - contour #0
|
||||||
|
moveTo(300, 700),
|
||||||
|
cubeTo(380, 700, 420, 580, 420, 500),
|
||||||
|
cubeTo(420, 350, 390, 100, 300, 100),
|
||||||
|
cubeTo(220, 100, 180, 220, 180, 300),
|
||||||
|
cubeTo(180, 450, 210, 700, 300, 700),
|
||||||
|
// - contour #1
|
||||||
|
moveTo(300, 800),
|
||||||
|
cubeTo(200, 800, 100, 580, 100, 400),
|
||||||
|
cubeTo(100, 220, 200, 0, 300, 0),
|
||||||
|
cubeTo(400, 0, 500, 220, 500, 400),
|
||||||
|
cubeTo(500, 580, 400, 800, 300, 800),
|
||||||
|
}, {
|
||||||
|
// one
|
||||||
|
// - contour #0
|
||||||
|
moveTo(100, 0),
|
||||||
|
lineTo(300, 0),
|
||||||
|
lineTo(300, 800),
|
||||||
|
lineTo(100, 800),
|
||||||
|
}, {
|
||||||
|
// uni4E2D
|
||||||
|
// - contour #0
|
||||||
|
moveTo(141, 520),
|
||||||
|
lineTo(137, 356),
|
||||||
|
lineTo(245, 400),
|
||||||
|
lineTo(331, 26),
|
||||||
|
lineTo(355, 414),
|
||||||
|
lineTo(463, 434),
|
||||||
|
lineTo(453, 620),
|
||||||
|
lineTo(341, 592),
|
||||||
|
lineTo(331, 758),
|
||||||
|
lineTo(243, 752),
|
||||||
|
lineTo(235, 562),
|
||||||
|
}}
|
||||||
|
|
||||||
if ng := f.NumGlyphs(); ng != len(wants) {
|
if ng := f.NumGlyphs(); ng != len(wants) {
|
||||||
t.Fatalf("NumGlyphs: got %d, want %d", ng, len(wants))
|
t.Fatalf("NumGlyphs: got %d, want %d", ng, len(wants))
|
||||||
}
|
}
|
||||||
|
var b Buffer
|
||||||
|
loop:
|
||||||
for i, want := range wants {
|
for i, want := range wants {
|
||||||
gd, err := f.viewGlyphData(nil, i)
|
if err := f.LoadGlyph(&b, GlyphIndex(i), nil); err != nil {
|
||||||
if err != nil {
|
t.Errorf("i=%d: LoadGlyph: %v", i, err)
|
||||||
t.Errorf("i=%d: %v", i, err)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if got := string(gd); got != want {
|
got := b.Segments
|
||||||
t.Errorf("i=%d:\ngot % x\nwant % x", i, got, want)
|
if len(got) != len(want) {
|
||||||
|
t.Errorf("i=%d: got %d elements, want %d\noverall:\ngot %v\nwant %v",
|
||||||
|
i, len(got), len(want), got, want)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for j, g := range got {
|
||||||
|
if w := want[j]; g != w {
|
||||||
|
t.Errorf("i=%d: element %d:\ngot %v\nwant %v\noverall:\ngot %v\nwant %v",
|
||||||
|
i, j, g, w, got, want)
|
||||||
|
continue loop
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user