font/sfnt: parse Type 2 Charstrings.
Change-Id: I61beec4611612800a519045e2552c513eb83c3f8
Reviewed-on: https://go-review.googlesource.com/33932
Reviewed-by: Dave Day <djd@golang.org>
diff --git a/font/sfnt/postscript.go b/font/sfnt/postscript.go
index cc277cb..93f5490 100644
--- a/font/sfnt/postscript.go
+++ b/font/sfnt/postscript.go
@@ -51,6 +51,8 @@
"fmt"
"math"
"strconv"
+
+ "golang.org/x/image/math/fixed"
)
const (
@@ -86,18 +88,7 @@
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 @@
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 {
- return nil, p.err
- }
+ 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 @@
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
- b0 := p.instructions[0]
- p.instructions = p.instructions[1:]
+const (
+ psContextTopDict psContext = iota
+ psContextType2Charstring
+)
- for b, escaped, operators := b0, false, topDictOperators[0]; ; {
- if b == escapeByte && !escaped {
- if len(p.instructions) <= 0 {
- p.err = errInvalidCFFTable
- return
- }
- b = p.instructions[0]
- p.instructions = p.instructions[1:]
- escaped = true
- operators = topDictOperators[1]
- continue
- }
+// psTopDictData contains fields specific to the Top DICT context.
+type psTopDictData struct {
+ charStrings int32
+}
- if int(b) < len(operators) {
- if op := operators[b]; op.name != "" {
- if p.stack.top < op.numPop {
- p.err = errInvalidCFFTable
- return
- }
- if op.run != nil {
- op.run(p)
- }
- if op.numPop < 0 {
- p.stack.top = 0
- } else {
- p.stack.top -= op.numPop
- }
- return
- }
- }
+func (d *psTopDictData) initialize() {
+ *d = psTopDictData{}
+}
- if escaped {
- p.err = 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
+// 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,
}
}
-type parseResult int32
+// 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
+}
-const (
- prUnsupportedRNE parseResult = -2
- prInvalid parseResult = -1
- prNone parseResult = +0
- prGood parseResult = +1
-)
+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 escaped, ops := false, psOperators[ctx][0]; ; {
+ if b == escapeByte && !escaped {
+ if len(p.instructions) <= 0 {
+ return errInvalidCFFTable
+ }
+ b = p.instructions[0]
+ p.instructions = p.instructions[1:]
+ escaped = true
+ ops = psOperators[ctx][1]
+ continue
+ }
+
+ if int(b) < len(ops) {
+ if op := ops[b]; op.name != "" {
+ if p.stack.top < op.numPop {
+ return errInvalidCFFTable
+ }
+ if op.run != nil {
+ if err := op.run(p); err != nil {
+ return err
+ }
+ }
+ if op.numPop < 0 {
+ p.stack.top = 0
+ } else {
+ p.stack.top -= op.numPop
+ }
+ continue loop
+ }
+ }
+
+ if escaped {
+ return fmt.Errorf("sfnt: unrecognized CFF 2-byte operator (12 %d)", b)
+ } else {
+ return fmt.Errorf("sfnt: unrecognized CFF 1-byte operator (%d)", b)
+ }
+ }
+ }
+ 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 @@
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 @@
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 @@
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,54 +485,364 @@
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{{
- // 1-byte operators.
- 0: {+1, "version", nil},
- 1: {+1, "Notice", nil},
- 2: {+1, "FullName", nil},
- 3: {+1, "FamilyName", nil},
- 4: {+1, "Weight", nil},
- 5: {-1, "FontBBox", nil},
- 13: {+1, "UniqueID", nil},
- 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]
+// 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},
+ 2: {+1, "FullName", nil},
+ 3: {+1, "FamilyName", nil},
+ 4: {+1, "Weight", nil},
+ 5: {-1, "FontBBox", nil},
+ 13: {+1, "UniqueID", nil},
+ 14: {-1, "XUID", nil},
+ 15: {+1, "charset", nil},
+ 16: {+1, "Encoding", nil},
+ 17: {+1, "CharStrings", func(p *psInterpreter) error {
+ p.topDict.charStrings = p.stack.a[p.stack.top-1]
+ return nil
+ }},
+ 18: {+2, "Private", nil},
+ }, {
+ // 2-byte operators. The first byte is the escape byte.
+ 0: {+1, "Copyright", nil},
+ 1: {+1, "isFixedPitch", nil},
+ 2: {+1, "ItalicAngle", nil},
+ 3: {+1, "UnderlinePosition", nil},
+ 4: {+1, "UnderlineThickness", nil},
+ 5: {+1, "PaintType", nil},
+ 6: {+1, "CharstringType", nil},
+ 7: {-1, "FontMatrix", nil},
+ 8: {+1, "StrokeWidth", nil},
+ 20: {+1, "SyntheticBase", nil},
+ 21: {+1, "PostScript", nil},
+ 22: {+1, "BaseFontName", nil},
+ 23: {-2, "BaseFontBlend", nil},
+ 30: {+3, "ROS", nil},
+ 31: {+1, "CIDFontVersion", nil},
+ 32: {+1, "CIDFontRevision", nil},
+ 33: {+1, "CIDFontType", nil},
+ 34: {+1, "CIDCount", nil},
+ 35: {+1, "UIDBase", nil},
+ 36: {+1, "FDArray", nil},
+ 37: {+1, "FDSelect", nil},
+ 38: {+1, "FontName", nil},
}},
- 18: {+2, "Private", nil},
-}, {
- // 2-byte operators. The first byte is the escape byte.
- 0: {+1, "Copyright", nil},
- 1: {+1, "isFixedPitch", nil},
- 2: {+1, "ItalicAngle", nil},
- 3: {+1, "UnderlinePosition", nil},
- 4: {+1, "UnderlineThickness", nil},
- 5: {+1, "PaintType", nil},
- 6: {+1, "CharstringType", nil},
- 7: {-1, "FontMatrix", nil},
- 8: {+1, "StrokeWidth", nil},
- 20: {+1, "SyntheticBase", nil},
- 21: {+1, "PostScript", nil},
- 22: {+1, "BaseFontName", nil},
- 23: {-2, "BaseFontBlend", nil},
- 30: {+3, "ROS", nil},
- 31: {+1, "CIDFontVersion", nil},
- 32: {+1, "CIDFontRevision", nil},
- 33: {+1, "CIDFontType", nil},
- 34: {+1, "CIDCount", nil},
- 35: {+1, "UIDBase", nil},
- 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
+}
diff --git a/font/sfnt/sfnt.go b/font/sfnt/sfnt.go
index 801cb0b..4ebf952 100644
--- a/font/sfnt/sfnt.go
+++ b/font/sfnt/sfnt.go
@@ -17,11 +17,14 @@
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 @@
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 @@
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 @@
}
// 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 @@
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
+)
diff --git a/font/sfnt/sfnt_test.go b/font/sfnt/sfnt_test.go
index 8c9df63..68ef19c 100644
--- a/font/sfnt/sfnt_test.go
+++ b/font/sfnt/sfnt_test.go
@@ -11,8 +11,43 @@
"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 @@
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
+ }
}
}
}