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:
Nigel Tao 2016-12-05 23:52:00 +11:00
parent ae63d5d566
commit ce50dba65c
3 changed files with 698 additions and 174 deletions

View File

@ -51,6 +51,8 @@ import (
"fmt"
"math"
"strconv"
"golang.org/x/image/math/fixed"
)
const (
@ -86,18 +88,7 @@ type cffParser struct {
buf []byte
locBuf [2]uint32
parseNumberBuf [maxRealNumberStrLen]byte
instructions []byte
stack struct {
a [psStackSize]int32
top int32
}
saved struct {
charStrings int32
}
psi psInterpreter
}
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])) {
return nil, p.err
}
for p.instructions = p.buf; len(p.instructions) > 0; {
p.step()
if p.err != nil {
p.psi.topDict.initialize()
if p.err = p.psi.run(psContextTopDict, p.buf); p.err != nil {
return nil, p.err
}
}
}
// 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
}
p.offset = p.base + int(p.saved.charStrings)
p.offset = p.base + int(p.psi.topDict.charStrings)
count, offSize, ok := p.parseIndexHeader()
if !ok {
return nil, p.err
@ -265,99 +253,129 @@ func (p *cffParser) parseIndexLocations(dst []uint32, count, offSize int32) (ok
return p.err == nil
}
// step executes a single operation, whether pushing a numeric operand onto the
// stack or executing an operator.
func (p *cffParser) step() {
if number, res := p.parseNumber(); res != prNone {
if res < 0 || p.stack.top == psStackSize {
if res == prUnsupportedRNE {
p.err = errUnsupportedRealNumberEncoding
} else {
p.err = errInvalidCFFTable
}
return
}
p.stack.a[p.stack.top] = number
p.stack.top++
return
type psContext uint32
const (
psContextTopDict psContext = iota
psContextType2Charstring
)
// psTopDictData contains fields specific to the Top DICT context.
type psTopDictData struct {
charStrings int32
}
b0 := p.instructions[0]
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,
}
}
// psInterpreter is a PostScript interpreter.
type psInterpreter struct {
ctx psContext
instructions []byte
stack struct {
a [psStackSize]int32
top int32
}
parseNumberBuf [maxRealNumberStrLen]byte
topDict psTopDictData
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
}
// Otherwise, execute an operator.
b := p.instructions[0]
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 len(p.instructions) <= 0 {
p.err = errInvalidCFFTable
return
return errInvalidCFFTable
}
b = p.instructions[0]
p.instructions = p.instructions[1:]
escaped = true
operators = topDictOperators[1]
ops = psOperators[ctx][1]
continue
}
if int(b) < len(operators) {
if op := operators[b]; op.name != "" {
if int(b) < len(ops) {
if op := ops[b]; op.name != "" {
if p.stack.top < op.numPop {
p.err = errInvalidCFFTable
return
return errInvalidCFFTable
}
if op.run != nil {
op.run(p)
if err := op.run(p); err != nil {
return err
}
}
if op.numPop < 0 {
p.stack.top = 0
} else {
p.stack.top -= op.numPop
}
return
continue loop
}
}
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 {
p.err = fmt.Errorf("sfnt: unrecognized CFF 1-byte operator (%d)", b)
}
return
return fmt.Errorf("sfnt: unrecognized CFF 1-byte operator (%d)", b)
}
}
type parseResult int32
const (
prUnsupportedRNE parseResult = -2
prInvalid parseResult = -1
prNone parseResult = +0
prGood parseResult = +1
)
}
return nil
}
// See 5176.CFF.pdf section 4 "DICT Data".
func (p *cffParser) parseNumber() (number int32, res parseResult) {
if len(p.instructions) == 0 {
return 0, prNone
}
switch b0 := p.instructions[0]; {
case b0 == 28:
func (p *psInterpreter) parseNumber() (hasResult bool, err error) {
number := int32(0)
switch b := p.instructions[0]; {
case b == 28:
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:]
return number, prGood
case b0 == 29:
case b == 29 && p.ctx == psContextTopDict:
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:]
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
// "Operand Encoding" but that table lists integer encodings. Further
// 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]
p.instructions = p.instructions[1:]
loop:
for {
if len(p.instructions) == 0 {
return 0, prInvalid
return true, errInvalidCFFTable
}
b := p.instructions[0]
p.instructions = p.instructions[1:]
@ -379,45 +398,60 @@ func (p *cffParser) parseNumber() (number int32, res parseResult) {
if nib == 0x0f {
f, err := strconv.ParseFloat(string(s), 32)
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 {
return 0, prInvalid
return true, errInvalidCFFTable
}
if len(s)+maxNibbleDefsLength > len(p.parseNumberBuf) {
return 0, prUnsupportedRNE
return true, errUnsupportedRealNumberEncoding
}
s = append(s, nibbleDefs[nib]...)
}
}
case b0 < 32:
case b < 32:
// No-op.
case b0 < 247:
case b < 247:
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 {
return 0, prInvalid
return true, errInvalidCFFTable
}
b1 := p.instructions[1]
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 {
return 0, prInvalid
return true, errInvalidCFFTable
}
b1 := p.instructions[1]
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-")
@ -442,7 +476,7 @@ var nibbleDefs = [16]string{
0x0f: "",
}
type cffOperator struct {
type psOperator struct {
// 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".
numPop int32
@ -451,13 +485,15 @@ type cffOperator struct {
name string
// run is the function that implements the operator. Nil means that we
// 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
// Operator Entries" and Table 10 "CIDFont Operator Extensions" used by this
// implementation.
var topDictOperators = [2][]cffOperator{{
// psOperators holds the 1-byte and 2-byte operators for PostScript interpreter
// contexts.
var psOperators = [...][2][]psOperator{
// 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.
0: {+1, "version", nil},
1: {+1, "Notice", nil},
@ -469,8 +505,9 @@ var topDictOperators = [2][]cffOperator{{
14: {-1, "XUID", nil},
15: {+1, "charset", nil},
16: {+1, "Encoding", nil},
17: {+1, "CharStrings", func(p *cffParser) {
p.saved.charStrings = p.stack.a[p.stack.top-1]
17: {+1, "CharStrings", func(p *psInterpreter) error {
p.topDict.charStrings = p.stack.a[p.stack.top-1]
return nil
}},
18: {+2, "Private", nil},
}, {
@ -497,8 +534,315 @@ var topDictOperators = [2][]cffOperator{{
36: {+1, "FDArray", nil},
37: {+1, "FDSelect", 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
// initial escape byte of 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
}

View File

@ -17,11 +17,14 @@ package sfnt // import "golang.org/x/image/font/sfnt"
import (
"errors"
"io"
"golang.org/x/image/math/fixed"
)
// These constants are not part of the specifications, but are limitations used
// by this implementation.
const (
maxHintBits = 256
maxNumTables = 256
maxRealNumberStrLen = 64 // Maximum length in bytes of the "-123.456E-7" representation.
@ -45,10 +48,15 @@ var (
errUnsupportedCFFVersion = errors.New("sfnt: unsupported CFF version")
errUnsupportedRealNumberEncoding = errors.New("sfnt: unsupported real number encoding")
errUnsupportedNumberOfHints = errors.New("sfnt: unsupported number of hints")
errUnsupportedNumberOfTables = errors.New("sfnt: unsupported number of tables")
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
// 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
@ -85,6 +93,16 @@ func (s *source) valid() bool {
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
// 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
@ -159,6 +177,11 @@ func ParseReaderAt(src io.ReaderAt) (*Font, error) {
}
// 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 {
src source
@ -343,11 +366,78 @@ func (f *Font) initialize() error {
return nil
}
func (f *Font) viewGlyphData(buf []byte, glyphIndex int) ([]byte, error) {
if glyphIndex < 0 || f.NumGlyphs() <= glyphIndex {
// TODO: func (f *Font) GlyphIndex(r rune) (x GlyphIndex, ok bool)
// 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
}
i := f.cached.locations[glyphIndex+0]
j := f.cached.locations[glyphIndex+1]
i := f.cached.locations[xx+0]
j := f.cached.locations[xx+1]
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
)

View File

@ -11,8 +11,43 @@ import (
"testing"
"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) {
f, err := Parse(goregular.TTF)
if err != nil {
@ -51,28 +86,83 @@ func TestPostScript(t *testing.T) {
t.Fatal(err)
}
// TODO: replace this by a higher level test, once we parse Type 2
// Charstrings.
// wants' vectors correspond 1-to-1 to what's in the CFFTest.sfd file,
// 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
// 5177.Type2.pdf Appendix A defines as "endchar".
wants := [...]string{
"\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",
"\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",
"\xf6\xa0\x76\x01\xef\xf7\x5c\x03\xef\x16\xf7\x5c\xf9\xb4\xfb\x5c\x06\x0e",
"\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",
}
// The .notdef glyph isn't explicitly in the SFD file, but for some unknown
// reason, FontForge generates a .notdef glyph in the OpenType/CFF file.
wants := [...][]Segment{{
// .notdef
// - contour #0
moveTo(50, 0),
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) {
t.Fatalf("NumGlyphs: got %d, want %d", ng, len(wants))
}
var b Buffer
loop:
for i, want := range wants {
gd, err := f.viewGlyphData(nil, i)
if err != nil {
t.Errorf("i=%d: %v", i, err)
if err := f.LoadGlyph(&b, GlyphIndex(i), nil); err != nil {
t.Errorf("i=%d: LoadGlyph: %v", i, err)
continue
}
if got := string(gd); got != want {
t.Errorf("i=%d:\ngot % x\nwant % x", i, got, want)
got := b.Segments
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
}
}
}
}