diff --git a/font/sfnt/sfnt.go b/font/sfnt/sfnt.go index 805b7a6..d4929a8 100644 --- a/font/sfnt/sfnt.go +++ b/font/sfnt/sfnt.go @@ -25,6 +25,7 @@ import ( // These constants are not part of the specifications, but are limitations used // by this implementation. const ( + maxGlyphDataLength = 64 * 1024 maxHintBits = 256 maxNumTables = 256 maxRealNumberStrLen = 64 // Maximum length in bytes of the "-123.456E-7" representation. @@ -40,7 +41,9 @@ var ( errInvalidBounds = errors.New("sfnt: invalid bounds") errInvalidCFFTable = errors.New("sfnt: invalid CFF table") + errInvalidGlyphData = errors.New("sfnt: invalid glyph data") errInvalidHeadTable = errors.New("sfnt: invalid head table") + errInvalidLocaTable = errors.New("sfnt: invalid loca table") errInvalidLocationData = errors.New("sfnt: invalid location data") errInvalidMaxpTable = errors.New("sfnt: invalid maxp table") errInvalidNameTable = errors.New("sfnt: invalid name table") @@ -51,6 +54,8 @@ var ( errInvalidVersion = errors.New("sfnt: invalid version") errUnsupportedCFFVersion = errors.New("sfnt: unsupported CFF version") + errUnsupportedCompoundGlyph = errors.New("sfnt: unsupported compound glyph") + errUnsupportedGlyphDataLength = errors.New("sfnt: unsupported glyph data length") errUnsupportedRealNumberEncoding = errors.New("sfnt: unsupported real number encoding") errUnsupportedNumberOfHints = errors.New("sfnt: unsupported number of hints") errUnsupportedNumberOfTables = errors.New("sfnt: unsupported number of tables") @@ -281,8 +286,9 @@ type Font struct { // TODO: hdmx, kern, vmtx? Others? cached struct { - isPostScript bool - unitsPerEm Units + indexToLocFormat bool // false means short, true means long. + isPostScript bool + unitsPerEm Units // The glyph data for the glyph index i is in // src[locations[i+0]:locations[i+1]]. @@ -388,6 +394,11 @@ func (f *Font) initialize() error { return errInvalidHeadTable } f.cached.unitsPerEm = Units(u) + u, err = f.src.u16(buf, f.head, 50) + if err != nil { + return err + } + f.cached.indexToLocFormat = u != 0 // https://www.microsoft.com/typography/otspec/maxp.htm if f.cached.isPostScript { @@ -417,8 +428,11 @@ func (f *Font) initialize() error { return err } } else { - // TODO: locaParser for TrueType fonts. - f.cached.locations = make([]uint32, numGlyphs+1) + f.cached.locations, err = parseLoca( + &f.src, f.loca, f.glyf.offset, f.cached.indexToLocFormat, numGlyphs) + if err != nil { + return err + } } if len(f.cached.locations) != numGlyphs+1 { return errInvalidLocationData @@ -436,6 +450,9 @@ func (f *Font) viewGlyphData(b *Buffer, x GlyphIndex) ([]byte, error) { } i := f.cached.locations[xx+0] j := f.cached.locations[xx+1] + if j-i > maxGlyphDataLength { + return nil, errUnsupportedGlyphDataLength + } return b.view(&f.src, int(i), int(j-i)) } @@ -467,7 +484,11 @@ func (f *Font) LoadGlyph(b *Buffer, x GlyphIndex, opts *LoadGlyphOptions) ([]Seg } b.segments = b.psi.type2Charstrings.segments } else { - return nil, errors.New("sfnt: TODO: load glyf data") + segments, err := appendGlyfSegments(b.segments, buf) + if err != nil { + return nil, err + } + b.segments = segments } // TODO: look at opts to scale / transform / hint the Buffer.segments. diff --git a/font/sfnt/sfnt_test.go b/font/sfnt/sfnt_test.go index f2c2805..4ffd72f 100644 --- a/font/sfnt/sfnt_test.go +++ b/font/sfnt/sfnt_test.go @@ -34,6 +34,18 @@ func lineTo(xa, ya int) Segment { } } +func quadTo(xa, ya, xb, yb int) Segment { + return Segment{ + Op: SegmentOpQuadTo, + Args: [6]fixed.Int26_6{ + 0: fixed.I(xa), + 1: fixed.I(ya), + 2: fixed.I(xb), + 3: fixed.I(yb), + }, + } +} + func cubeTo(xa, ya, xb, yb, xc, yc int) Segment { return Segment{ Op: SegmentOpCubeTo, @@ -76,16 +88,7 @@ func testTrueType(t *testing.T, f *Font) { } } -func TestPostScript(t *testing.T) { - data, err := ioutil.ReadFile(filepath.Join("..", "testdata", "CFFTest.otf")) - if err != nil { - t.Fatal(err) - } - f, err := Parse(data) - if err != nil { - t.Fatal(err) - } - +func TestPostScriptSegments(t *testing.T) { // wants' vectors correspond 1-to-1 to what's in the CFFTest.sfd file, // although OpenType/CFF and FontForge's SFD have reversed orders. // https://fontforge.github.io/validation.html says that "All paths must be @@ -96,8 +99,8 @@ func TestPostScript(t *testing.T) { // again when it saves them, of course)." // // 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{{ + // reason, FontForge generates it in the OpenType/CFF file. + wants := [][]Segment{{ // .notdef // - contour #0 moveTo(50, 0), @@ -158,8 +161,80 @@ func TestPostScript(t *testing.T) { lineTo(331, 758), lineTo(243, 752), lineTo(235, 562), + // TODO: explicitly (not implicitly) close these contours? }} + testSegments(t, "CFFTest.otf", wants) +} + +func TestTrueTypeSegments(t *testing.T) { + // wants' vectors correspond 1-to-1 to what's in the glyfTest.sfd file, + // although FontForge's SFD format stores quadratic Bézier curves as cubics + // with duplicated off-curve points. quadTo(bx, by, cx, cy) is stored as + // "bx by bx by cx cy". + // + // The .notdef, .null and nonmarkingreturn glyphs aren't explicitly in the + // SFD file, but for some unknown reason, FontForge generates them in the + // TrueType file. + wants := [][]Segment{{ + // .notdef + // - contour #0 + moveTo(68, 0), + lineTo(68, 1365), + lineTo(612, 1365), + lineTo(612, 0), + lineTo(68, 0), + // - contour #1 + moveTo(136, 68), + lineTo(544, 68), + lineTo(544, 1297), + lineTo(136, 1297), + lineTo(136, 68), + }, { + // .null + // Empty glyph. + }, { + // nonmarkingreturn + // Empty glyph. + }, { + // zero + // - contour #0 + moveTo(614, 1434), + quadTo(369, 1434, 369, 614), + quadTo(369, 471, 435, 338), + quadTo(502, 205, 614, 205), + quadTo(860, 205, 860, 1024), + quadTo(860, 1167, 793, 1300), + quadTo(727, 1434, 614, 1434), + // - contour #1 + moveTo(614, 1638), + quadTo(1024, 1638, 1024, 819), + quadTo(1024, 0, 614, 0), + quadTo(205, 0, 205, 819), + quadTo(205, 1638, 614, 1638), + }, { + // one + // - contour #0 + moveTo(205, 0), + lineTo(205, 1638), + lineTo(614, 1638), + lineTo(614, 0), + lineTo(205, 0), + }} + + testSegments(t, "glyfTest.ttf", wants) +} + +func testSegments(t *testing.T, filename string, wants [][]Segment) { + data, err := ioutil.ReadFile(filepath.Join("..", "testdata", filename)) + if err != nil { + t.Fatal(err) + } + f, err := Parse(data) + if err != nil { + t.Fatal(err) + } + if ng := f.NumGlyphs(); ng != len(wants) { t.Fatalf("NumGlyphs: got %d, want %d", ng, len(wants)) } @@ -191,7 +266,7 @@ loop: name, err := f.Name(nil, NameIDFamily) if err != nil { t.Errorf("Name: %v", err) - } else if want := "CFFTest"; name != want { + } else if want := filename[:len(filename)-len(".ttf")]; name != want { t.Errorf("Name:\ngot %q\nwant %q", name, want) } } diff --git a/font/sfnt/truetype.go b/font/sfnt/truetype.go new file mode 100644 index 0000000..851904d --- /dev/null +++ b/font/sfnt/truetype.go @@ -0,0 +1,489 @@ +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package sfnt + +import ( + "golang.org/x/image/math/fixed" +) + +// Flags for simple (non-compound) glyphs. +// +// See https://www.microsoft.com/typography/OTSPEC/glyf.htm +const ( + flagOnCurve = 1 << 0 // 0x0001 + flagXShortVector = 1 << 1 // 0x0002 + flagYShortVector = 1 << 2 // 0x0004 + flagRepeat = 1 << 3 // 0x0008 + + // The same flag bits are overloaded to have two meanings, dependent on the + // value of the flag{X,Y}ShortVector bits. + flagPositiveXShortVector = 1 << 4 // 0x0010 + flagThisXIsSame = 1 << 4 // 0x0010 + flagPositiveYShortVector = 1 << 5 // 0x0020 + flagThisYIsSame = 1 << 5 // 0x0020 +) + +// Flags for compound glyphs. +// +// See https://www.microsoft.com/typography/OTSPEC/glyf.htm +const ( + flagArg1And2AreWords = 1 << 0 // 0x0001 + flagArgsAreXYValues = 1 << 1 // 0x0002 + flagRoundXYToGrid = 1 << 2 // 0x0004 + flagWeHaveAScale = 1 << 3 // 0x0008 + flagReserved4 = 1 << 4 // 0x0010 + flagMoreComponents = 1 << 5 // 0x0020 + flagWeHaveAnXAndYScale = 1 << 6 // 0x0040 + flagWeHaveATwoByTwo = 1 << 7 // 0x0080 + flagWeHaveInstructions = 1 << 8 // 0x0100 + flagUseMyMetrics = 1 << 9 // 0x0200 + flagOverlapCompound = 1 << 10 // 0x0400 + flagScaledComponentOffset = 1 << 11 // 0x0800 + flagUnscaledComponentOffset = 1 << 12 // 0x1000 +) + +func midPoint(p, q fixed.Point26_6) fixed.Point26_6 { + return fixed.Point26_6{ + X: (p.X + q.X) / 2, + Y: (p.Y + q.Y) / 2, + } +} + +func parseLoca(src *source, loca table, glyfOffset uint32, indexToLocFormat bool, numGlyphs int) (locations []uint32, err error) { + if indexToLocFormat { + if loca.length != 4*uint32(numGlyphs+1) { + return nil, errInvalidLocaTable + } + } else { + if loca.length != 2*uint32(numGlyphs+1) { + return nil, errInvalidLocaTable + } + } + + locations = make([]uint32, numGlyphs+1) + buf, err := src.view(nil, int(loca.offset), int(loca.length)) + if err != nil { + return nil, err + } + + if indexToLocFormat { + for i := range locations { + locations[i] = 1*uint32(u32(buf[4*i:])) + glyfOffset + } + } else { + for i := range locations { + locations[i] = 2*uint32(u16(buf[2*i:])) + glyfOffset + } + } + return locations, err +} + +// https://www.microsoft.com/typography/OTSPEC/glyf.htm says that "Each +// glyph begins with the following [10 byte] header". +const glyfHeaderLen = 10 + +// appendGlyfSegments appends to dst the segments encoded in the glyf data. +func appendGlyfSegments(dst []Segment, data []byte) ([]Segment, error) { + if len(data) == 0 { + return dst, nil + } + if len(data) < glyfHeaderLen { + return nil, errInvalidGlyphData + } + index := glyfHeaderLen + + numContours, numPoints := int16(u16(data)), 0 + switch { + case numContours == -1: + // We have a compound glyph. No-op. + case numContours == 0: + return dst, nil + case numContours > 0: + // We have a simple (non-compound) glyph. + index += 2 * int(numContours) + if index > len(data) { + return nil, errInvalidGlyphData + } + // The +1 for numPoints is because the value in the file format is + // inclusive, but Go's slice[:index] semantics are exclusive. + numPoints = 1 + int(u16(data[index-2:])) + default: + return nil, errInvalidGlyphData + } + + // Skip the hinting instructions. + index += 2 + if index > len(data) { + return nil, errInvalidGlyphData + } + hintsLength := int(u16(data[index-2:])) + index += hintsLength + if index > len(data) { + return nil, errInvalidGlyphData + } + + // TODO: support compound glyphs. + if numContours < 0 { + return nil, errUnsupportedCompoundGlyph + } + + // For simple (non-compound) glyphs, the remainder of the glyf data + // consists of (flags, x, y) points: the Bézier curve segments. These are + // stored in columns (all the flags first, then all the x co-ordinates, + // then all the y co-ordinates), not rows, as it compresses better. + // + // Decoding those points in row order involves two passes. The first pass + // determines the indexes (relative to the data slice) of where the flags, + // the x co-ordinates and the y co-ordinates each start. + flagIndex := int32(index) + xIndex, yIndex, ok := findXYIndexes(data, index, numPoints) + if !ok { + return nil, errInvalidGlyphData + } + + // The second pass decodes each (flags, x, y) tuple in row order. + g := glyfIter{ + data: data, + flagIndex: flagIndex, + xIndex: xIndex, + yIndex: yIndex, + endIndex: glyfHeaderLen, + // The -1 is because the contour-end index in the file format is + // inclusive, but Go's slice[:index] semantics are exclusive. + prevEnd: -1, + numContours: int32(numContours), + } + for g.nextContour() { + for g.nextSegment() { + dst = append(dst, g.seg) + } + } + if g.err != nil { + return nil, g.err + } + return dst, nil +} + +func findXYIndexes(data []byte, index, numPoints int) (xIndex, yIndex int32, ok bool) { + xDataLen := 0 + yDataLen := 0 + for i := 0; ; { + if i > numPoints { + return 0, 0, false + } + if i == numPoints { + break + } + + repeatCount := 1 + if index >= len(data) { + return 0, 0, false + } + flag := data[index] + index++ + if flag&flagRepeat != 0 { + if index >= len(data) { + return 0, 0, false + } + repeatCount += int(data[index]) + index++ + } + + xSize := 0 + if flag&flagXShortVector != 0 { + xSize = 1 + } else if flag&flagThisXIsSame == 0 { + xSize = 2 + } + xDataLen += xSize * repeatCount + + ySize := 0 + if flag&flagYShortVector != 0 { + ySize = 1 + } else if flag&flagThisYIsSame == 0 { + ySize = 2 + } + yDataLen += ySize * repeatCount + + i += repeatCount + } + if index+xDataLen+yDataLen > len(data) { + return 0, 0, false + } + return int32(index), int32(index + xDataLen), true +} + +type glyfIter struct { + data []byte + err error + + // Various indices into the data slice. See the "Decoding those points in + // row order" comment above. + flagIndex int32 + xIndex int32 + yIndex int32 + + // endIndex points to the uint16 that is the inclusive point index of the + // current contour's end. prevEnd is the previous contour's end. + endIndex int32 + prevEnd int32 + + // c and p count the current contour and point, up to numContours and + // numPoints. + c, numContours int32 + p, nPoints int32 + + // The next two groups of fields track points and segments. Points are what + // the underlying file format provides. Bézier curve segments are what the + // rasterizer consumes. + // + // Points are either on-curve or off-curve. Two consecutive on-curve points + // define a linear curve segment between them. N off-curve points between + // on-curve points define N quadratic curve segments. The TrueType glyf + // format does not use cubic curves. If N is greater than 1, some of these + // segment end points are implicit, the midpoint of two off-curve points. + // Given the points A, B1, B2, ..., BN, C, where A and C are on-curve and + // all the Bs are off-curve, the segments are: + // + // - A, B1, midpoint(B1, B2) + // - midpoint(B1, B2), B2, midpoint(B2, B3) + // - midpoint(B2, B3), B3, midpoint(B3, B4) + // - ... + // - midpoint(BN-1, BN), BN, C + // + // Note that the sequence of Bs may wrap around from the last point in the + // glyf data to the first. A and C may also be the same point (the only + // explicit on-curve point), or there may be no explicit on-curve points at + // all (but still implicit ones between explicit off-curve points). + + // Points. + x, y int16 + on bool + flag uint8 + repeats uint8 + + // Segments. + closing bool + closed bool + firstOnCurveValid bool + firstOffCurveValid bool + lastOffCurveValid bool + firstOnCurve fixed.Point26_6 + firstOffCurve fixed.Point26_6 + lastOffCurve fixed.Point26_6 + seg Segment +} + +func (g *glyfIter) nextContour() (ok bool) { + if g.c == g.numContours { + return false + } + g.c++ + + end := int32(u16(g.data[g.endIndex:])) + g.endIndex += 2 + if end <= g.prevEnd { + g.err = errInvalidGlyphData + return false + } + g.nPoints = end - g.prevEnd + g.p = 0 + g.prevEnd = end + + g.closing = false + g.closed = false + g.firstOnCurveValid = false + g.firstOffCurveValid = false + g.lastOffCurveValid = false + + return true +} + +func (g *glyfIter) close() { + switch { + case !g.firstOffCurveValid && !g.lastOffCurveValid: + g.closed = true + g.seg = Segment{ + Op: SegmentOpLineTo, + Args: [6]fixed.Int26_6{ + g.firstOnCurve.X, + g.firstOnCurve.Y, + }, + } + case !g.firstOffCurveValid && g.lastOffCurveValid: + g.closed = true + g.seg = Segment{ + Op: SegmentOpQuadTo, + Args: [6]fixed.Int26_6{ + g.lastOffCurve.X, + g.lastOffCurve.Y, + g.firstOnCurve.X, + g.firstOnCurve.Y, + }, + } + case g.firstOffCurveValid && !g.lastOffCurveValid: + g.closed = true + g.seg = Segment{ + Op: SegmentOpQuadTo, + Args: [6]fixed.Int26_6{ + g.firstOffCurve.X, + g.firstOffCurve.Y, + g.firstOnCurve.X, + g.firstOnCurve.Y, + }, + } + case g.firstOffCurveValid && g.lastOffCurveValid: + mid := midPoint(g.lastOffCurve, g.firstOffCurve) + g.lastOffCurveValid = false + g.seg = Segment{ + Op: SegmentOpQuadTo, + Args: [6]fixed.Int26_6{ + g.lastOffCurve.X, + g.lastOffCurve.Y, + mid.X, + mid.Y, + }, + } + } +} + +func (g *glyfIter) nextSegment() (ok bool) { + for !g.closed { + if g.closing || !g.nextPoint() { + g.closing = true + g.close() + return true + } + + p := fixed.Point26_6{ + X: fixed.Int26_6(g.x) << 6, + Y: fixed.Int26_6(g.y) << 6, + } + + if !g.firstOnCurveValid { + if g.on { + g.firstOnCurve = p + g.firstOnCurveValid = true + g.seg = Segment{ + Op: SegmentOpMoveTo, + Args: [6]fixed.Int26_6{ + p.X, + p.Y, + }, + } + return true + } else if !g.firstOffCurveValid { + g.firstOffCurve = p + g.firstOffCurveValid = true + continue + } else { + midp := midPoint(g.firstOffCurve, p) + g.firstOnCurve = midp + g.firstOnCurveValid = true + g.lastOffCurve = p + g.lastOffCurveValid = true + g.seg = Segment{ + Op: SegmentOpMoveTo, + Args: [6]fixed.Int26_6{ + midp.X, + midp.Y, + }, + } + return true + } + + } else if !g.lastOffCurveValid { + if !g.on { + g.lastOffCurve = p + g.lastOffCurveValid = true + continue + } else { + g.seg = Segment{ + Op: SegmentOpLineTo, + Args: [6]fixed.Int26_6{ + p.X, + p.Y, + }, + } + return true + } + + } else { + if !g.on { + midp := midPoint(g.lastOffCurve, p) + g.seg = Segment{ + Op: SegmentOpQuadTo, + Args: [6]fixed.Int26_6{ + g.lastOffCurve.X, + g.lastOffCurve.Y, + midp.X, + midp.Y, + }, + } + g.lastOffCurve = p + g.lastOffCurveValid = true + return true + } else { + g.seg = Segment{ + Op: SegmentOpQuadTo, + Args: [6]fixed.Int26_6{ + g.lastOffCurve.X, + g.lastOffCurve.Y, + p.X, + p.Y, + }, + } + g.lastOffCurveValid = false + return true + } + } + } + return false +} + +func (g *glyfIter) nextPoint() (ok bool) { + if g.p == g.nPoints { + return false + } + g.p++ + + if g.repeats > 0 { + g.repeats-- + } else { + g.flag = g.data[g.flagIndex] + g.flagIndex++ + if g.flag&flagRepeat != 0 { + g.repeats = g.data[g.flagIndex] + g.flagIndex++ + } + } + + if g.flag&flagXShortVector != 0 { + if g.flag&flagPositiveXShortVector != 0 { + g.x += int16(g.data[g.xIndex]) + } else { + g.x -= int16(g.data[g.xIndex]) + } + g.xIndex += 1 + } else if g.flag&flagThisXIsSame == 0 { + g.x += int16(u16(g.data[g.xIndex:])) + g.xIndex += 2 + } + + if g.flag&flagYShortVector != 0 { + if g.flag&flagPositiveYShortVector != 0 { + g.y += int16(g.data[g.yIndex]) + } else { + g.y -= int16(g.data[g.yIndex]) + } + g.yIndex += 1 + } else if g.flag&flagThisYIsSame == 0 { + g.y += int16(u16(g.data[g.yIndex:])) + g.yIndex += 2 + } + + g.on = g.flag&flagOnCurve != 0 + return true +} diff --git a/font/testdata/glyfTest.sfd b/font/testdata/glyfTest.sfd new file mode 100644 index 0000000..e61c54c --- /dev/null +++ b/font/testdata/glyfTest.sfd @@ -0,0 +1,102 @@ +SplineFontDB: 3.0 +FontName: glyfTest +FullName: glyfTest +FamilyName: glyfTest +Weight: Regular +Copyright: Copyright 2016 The Go Authors. All rights reserved.\nUse of this font is governed by a BSD-style license that can be found at https://golang.org/LICENSE. +Version: 001.000 +ItalicAngle: -11.25 +UnderlinePosition: -204 +UnderlineWidth: 102 +Ascent: 1638 +Descent: 410 +LayerCount: 2 +Layer: 0 1 "Back" 1 +Layer: 1 1 "Fore" 0 +XUID: [1021 367 888937226 7862908] +FSType: 8 +OS2Version: 0 +OS2_WeightWidthSlopeOnly: 0 +OS2_UseTypoMetrics: 1 +CreationTime: 1484386143 +ModificationTime: 1484386143 +PfmFamily: 17 +TTFWeight: 400 +TTFWidth: 5 +LineGap: 184 +VLineGap: 0 +OS2TypoAscent: 0 +OS2TypoAOffset: 1 +OS2TypoDescent: 0 +OS2TypoDOffset: 1 +OS2TypoLinegap: 184 +OS2WinAscent: 0 +OS2WinAOffset: 1 +OS2WinDescent: 0 +OS2WinDOffset: 1 +HheadAscent: 0 +HheadAOffset: 1 +HheadDescent: 0 +HheadDOffset: 1 +OS2Vendor: 'PfEd' +MarkAttachClasses: 1 +DEI: 91125 +LangName: 1033 +Encoding: UnicodeBmp +UnicodeInterp: none +NameList: Adobe Glyph List +DisplaySize: -24 +AntiAlias: 1 +FitToEm: 1 +WinInfo: 0 32 23 +BeginPrivate: 0 +EndPrivate +TeXData: 1 0 0 346030 173015 115343 0 -1048576 115343 783286 444596 497025 792723 393216 433062 380633 303038 157286 324010 404750 52429 2506097 1059062 262144 +BeginChars: 65536 2 + +StartChar: zero +Encoding: 48 48 0 +Width: 1228 +VWidth: 0 +Flags: W +HStem: 0 205<508 700> 1434 205<529 720> +VStem: 205 164<500 1088> 860 164<550 1139> +LayerCount: 2 +Fore +SplineSet +614 1434 m 0,0,1 + 369 1434 369 1434 369 614 c 0,2,3 + 369 471 369 471 435 338 c 0,4,5 + 502 205 502 205 614 205 c 0,6,7 + 860 205 860 205 860 1024 c 0,8,9 + 860 1167 860 1167 793 1300 c 0,10,11 + 727 1434 727 1434 614 1434 c 0,0,1 +614 1638 m 0,12,13 + 1024 1638 1024 1638 1024 819 c 128,-1,14 + 1024 0 1024 0 614 0 c 0,15,16 + 205 0 205 0 205 819 c 128,-1,17 + 205 1638 205 1638 614 1638 c 0,12,13 +EndSplineSet +Validated: 1 +EndChar + +StartChar: one +Encoding: 49 49 1 +Width: 819 +VWidth: 0 +Flags: W +HStem: 0 43G<205 614> +VStem: 205 410<0 1638> +LayerCount: 2 +Fore +SplineSet +205 0 m 25,0,-1 + 205 1638 l 1,1,-1 + 614 1638 l 1,2,-1 + 614 0 l 1,3,-1 + 205 0 l 25,0,-1 +EndSplineSet +Validated: 1 +EndChar +EndChars +EndSplineFont diff --git a/font/testdata/glyfTest.ttf b/font/testdata/glyfTest.ttf new file mode 100644 index 0000000..587b3fe Binary files /dev/null and b/font/testdata/glyfTest.ttf differ