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 @@ 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 { - 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 @@ 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 - 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 @@ 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,54 +485,364 @@ 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{{ - // 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 @@ 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 +) 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 @@ 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 + } } } }