d9c2484c48
Currently the library only parses the version in PostScript table. However use cases such as PDF document processing requires this information to be exposed. CL parses a minimal set of the fields from the PostScript table and exposes it via new PostTable method. Change-Id: Ia86eecea9f5aaf557c7e4737f2474966aa30cff2 Reviewed-on: https://go-review.googlesource.com/c/145797 Reviewed-by: Nigel Tao <nigeltao@golang.org>
867 lines
20 KiB
Go
867 lines
20 KiB
Go
// Copyright 2016 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 (
|
|
"bytes"
|
|
"fmt"
|
|
"image"
|
|
"io/ioutil"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"golang.org/x/image/font"
|
|
"golang.org/x/image/font/gofont/gobold"
|
|
"golang.org/x/image/font/gofont/gomono"
|
|
"golang.org/x/image/font/gofont/goregular"
|
|
"golang.org/x/image/math/fixed"
|
|
)
|
|
|
|
func pt(x, y fixed.Int26_6) fixed.Point26_6 {
|
|
return fixed.Point26_6{X: x, Y: y}
|
|
}
|
|
|
|
func moveTo(xa, ya fixed.Int26_6) Segment {
|
|
return Segment{
|
|
Op: SegmentOpMoveTo,
|
|
Args: [3]fixed.Point26_6{pt(xa, ya)},
|
|
}
|
|
}
|
|
|
|
func lineTo(xa, ya fixed.Int26_6) Segment {
|
|
return Segment{
|
|
Op: SegmentOpLineTo,
|
|
Args: [3]fixed.Point26_6{pt(xa, ya)},
|
|
}
|
|
}
|
|
|
|
func quadTo(xa, ya, xb, yb fixed.Int26_6) Segment {
|
|
return Segment{
|
|
Op: SegmentOpQuadTo,
|
|
Args: [3]fixed.Point26_6{pt(xa, ya), pt(xb, yb)},
|
|
}
|
|
}
|
|
|
|
func cubeTo(xa, ya, xb, yb, xc, yc fixed.Int26_6) Segment {
|
|
return Segment{
|
|
Op: SegmentOpCubeTo,
|
|
Args: [3]fixed.Point26_6{pt(xa, ya), pt(xb, yb), pt(xc, yc)},
|
|
}
|
|
}
|
|
|
|
func translate(dx, dy fixed.Int26_6, s Segment) Segment {
|
|
translateArgs(&s.Args, dx, dy)
|
|
return s
|
|
}
|
|
|
|
func transform(txx, txy, tyx, tyy int16, dx, dy fixed.Int26_6, s Segment) Segment {
|
|
transformArgs(&s.Args, txx, txy, tyx, tyy, dx, dy)
|
|
return s
|
|
}
|
|
|
|
func checkSegmentsEqual(got, want []Segment) error {
|
|
// Flip got's Y axis. The test cases' coordinates are given with the Y axis
|
|
// increasing up, as that is what the ttx tool gives, and is the model for
|
|
// the underlying font format. The Go API returns coordinates with the Y
|
|
// axis increasing down, the same as the standard graphics libraries.
|
|
for i := range got {
|
|
for j := range got[i].Args {
|
|
got[i].Args[j].Y *= -1
|
|
}
|
|
}
|
|
|
|
if len(got) != len(want) {
|
|
return fmt.Errorf("got %d elements, want %d\noverall:\ngot %v\nwant %v",
|
|
len(got), len(want), got, want)
|
|
}
|
|
for i, g := range got {
|
|
if w := want[i]; g != w {
|
|
return fmt.Errorf("element %d:\ngot %v\nwant %v\noverall:\ngot %v\nwant %v",
|
|
i, g, w, got, want)
|
|
}
|
|
}
|
|
|
|
// Check that every contour is closed.
|
|
if len(got) == 0 {
|
|
return nil
|
|
}
|
|
if got[0].Op != SegmentOpMoveTo {
|
|
return fmt.Errorf("segments do not start with a moveTo")
|
|
}
|
|
var (
|
|
first, last fixed.Point26_6
|
|
firstI int
|
|
)
|
|
checkClosed := func(lastI int) error {
|
|
if first != last {
|
|
return fmt.Errorf("segments[%d:%d] not closed:\nfirst %v\nlast %v", firstI, lastI, first, last)
|
|
}
|
|
return nil
|
|
}
|
|
for i, g := range got {
|
|
switch g.Op {
|
|
case SegmentOpMoveTo:
|
|
if i != 0 {
|
|
if err := checkClosed(i); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
firstI, first, last = i, g.Args[0], g.Args[0]
|
|
case SegmentOpLineTo:
|
|
last = g.Args[0]
|
|
case SegmentOpQuadTo:
|
|
last = g.Args[1]
|
|
case SegmentOpCubeTo:
|
|
last = g.Args[2]
|
|
}
|
|
}
|
|
return checkClosed(len(got))
|
|
}
|
|
|
|
func TestTrueTypeParse(t *testing.T) {
|
|
f, err := Parse(goregular.TTF)
|
|
if err != nil {
|
|
t.Fatalf("Parse: %v", err)
|
|
}
|
|
testTrueType(t, f)
|
|
}
|
|
|
|
func TestTrueTypeParseReaderAt(t *testing.T) {
|
|
f, err := ParseReaderAt(bytes.NewReader(goregular.TTF))
|
|
if err != nil {
|
|
t.Fatalf("ParseReaderAt: %v", err)
|
|
}
|
|
testTrueType(t, f)
|
|
}
|
|
|
|
func testTrueType(t *testing.T, f *Font) {
|
|
if got, want := f.UnitsPerEm(), Units(2048); got != want {
|
|
t.Errorf("UnitsPerEm: got %d, want %d", got, want)
|
|
}
|
|
// The exact number of glyphs in goregular.TTF can vary, and future
|
|
// versions may add more glyphs, but https://blog.golang.org/go-fonts says
|
|
// that "The WGL4 character set... [has] more than 650 characters in all.
|
|
if got, want := f.NumGlyphs(), 650; got <= want {
|
|
t.Errorf("NumGlyphs: got %d, want > %d", got, want)
|
|
}
|
|
}
|
|
|
|
func fontData(name string) []byte {
|
|
switch name {
|
|
case "gobold":
|
|
return gobold.TTF
|
|
case "gomono":
|
|
return gomono.TTF
|
|
case "goregular":
|
|
return goregular.TTF
|
|
}
|
|
panic("unreachable")
|
|
}
|
|
|
|
func TestBounds(t *testing.T) {
|
|
testCases := map[string]fixed.Rectangle26_6{
|
|
"gobold": {
|
|
Min: fixed.Point26_6{
|
|
X: -452,
|
|
Y: -2193,
|
|
},
|
|
Max: fixed.Point26_6{
|
|
X: 2190,
|
|
Y: 432,
|
|
},
|
|
},
|
|
"gomono": {
|
|
Min: fixed.Point26_6{
|
|
X: 0,
|
|
Y: -2227,
|
|
},
|
|
Max: fixed.Point26_6{
|
|
X: 1229,
|
|
Y: 432,
|
|
},
|
|
},
|
|
"goregular": {
|
|
Min: fixed.Point26_6{
|
|
X: -440,
|
|
Y: -2118,
|
|
},
|
|
Max: fixed.Point26_6{
|
|
X: 2160,
|
|
Y: 543,
|
|
},
|
|
},
|
|
}
|
|
|
|
var b Buffer
|
|
for name, want := range testCases {
|
|
f, err := Parse(fontData(name))
|
|
if err != nil {
|
|
t.Errorf("Parse(%q): %v", name, err)
|
|
continue
|
|
}
|
|
ppem := fixed.Int26_6(f.UnitsPerEm())
|
|
|
|
got, err := f.Bounds(&b, ppem, font.HintingNone)
|
|
if err != nil {
|
|
t.Errorf("name=%q: Bounds: %v", name, err)
|
|
continue
|
|
}
|
|
if got != want {
|
|
t.Errorf("name=%q: Bounds: got %v, want %v", name, got, want)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMetrics(t *testing.T) {
|
|
cmapFont, err := ioutil.ReadFile(filepath.FromSlash("../testdata/cmapTest.ttf"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
testCases := map[string]struct {
|
|
font []byte
|
|
want font.Metrics
|
|
}{
|
|
"goregular": {goregular.TTF, font.Metrics{Height: 2367, Ascent: 1935, Descent: 432, XHeight: 1086, CapHeight: 1480,
|
|
CaretSlope: image.Point{X: 0, Y: 1}}},
|
|
// cmapTest.ttf has a non-zero lineGap.
|
|
"cmapTest": {cmapFont, font.Metrics{Height: 1549, Ascent: 1365, Descent: 0, XHeight: 800, CapHeight: 800,
|
|
CaretSlope: image.Point{X: 20, Y: 100}}},
|
|
}
|
|
var b Buffer
|
|
for name, tc := range testCases {
|
|
f, err := Parse(tc.font)
|
|
if err != nil {
|
|
t.Errorf("name=%q: Parse: %v", name, err)
|
|
continue
|
|
}
|
|
ppem := fixed.Int26_6(f.UnitsPerEm())
|
|
|
|
got, err := f.Metrics(&b, ppem, font.HintingNone)
|
|
if err != nil {
|
|
t.Errorf("name=%q: Metrics: %v", name, err)
|
|
continue
|
|
}
|
|
if got != tc.want {
|
|
t.Errorf("name=%q: Metrics: got %v, want %v", name, got, tc.want)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGlyphAdvance(t *testing.T) {
|
|
testCases := map[string][]struct {
|
|
r rune
|
|
want fixed.Int26_6
|
|
}{
|
|
"gobold": {
|
|
{' ', 569},
|
|
{'A', 1479},
|
|
{'Á', 1479},
|
|
{'Æ', 2048},
|
|
{'i', 592},
|
|
{'x', 1139},
|
|
},
|
|
"gomono": {
|
|
{' ', 1229},
|
|
{'A', 1229},
|
|
{'Á', 1229},
|
|
{'Æ', 1229},
|
|
{'i', 1229},
|
|
{'x', 1229},
|
|
},
|
|
"goregular": {
|
|
{' ', 569},
|
|
{'A', 1366},
|
|
{'Á', 1366},
|
|
{'Æ', 2048},
|
|
{'i', 505},
|
|
{'x', 1024},
|
|
},
|
|
}
|
|
|
|
var b Buffer
|
|
for name, testCases1 := range testCases {
|
|
f, err := Parse(fontData(name))
|
|
if err != nil {
|
|
t.Errorf("Parse(%q): %v", name, err)
|
|
continue
|
|
}
|
|
ppem := fixed.Int26_6(f.UnitsPerEm())
|
|
|
|
for _, tc := range testCases1 {
|
|
x, err := f.GlyphIndex(&b, tc.r)
|
|
if err != nil {
|
|
t.Errorf("name=%q, r=%q: GlyphIndex: %v", name, tc.r, err)
|
|
continue
|
|
}
|
|
got, err := f.GlyphAdvance(&b, x, ppem, font.HintingNone)
|
|
if err != nil {
|
|
t.Errorf("name=%q, r=%q: GlyphAdvance: %v", name, tc.r, err)
|
|
continue
|
|
}
|
|
if got != tc.want {
|
|
t.Errorf("name=%q, r=%q: GlyphAdvance: got %d, want %d", name, tc.r, got, tc.want)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGoRegularGlyphIndex(t *testing.T) {
|
|
f, err := Parse(goregular.TTF)
|
|
if err != nil {
|
|
t.Fatalf("Parse: %v", err)
|
|
}
|
|
|
|
testCases := []struct {
|
|
r rune
|
|
want GlyphIndex
|
|
}{
|
|
// Glyphs that aren't present in Go Regular.
|
|
{'\u001f', 0}, // U+001F <control>
|
|
{'\u0200', 0}, // U+0200 LATIN CAPITAL LETTER A WITH DOUBLE GRAVE
|
|
{'\u2000', 0}, // U+2000 EN QUAD
|
|
|
|
// The want values below can be verified by running the ttx tool on
|
|
// Go-Regular.ttf.
|
|
//
|
|
// The actual values are ad hoc, and result from whatever tools the
|
|
// Bigelow & Holmes type foundry used and the order in which they
|
|
// crafted the glyphs. They may change over time as newer versions of
|
|
// the font are released.
|
|
|
|
{'\u0020', 3}, // U+0020 SPACE
|
|
{'\u0021', 4}, // U+0021 EXCLAMATION MARK
|
|
{'\u0022', 5}, // U+0022 QUOTATION MARK
|
|
{'\u0023', 6}, // U+0023 NUMBER SIGN
|
|
{'\u0024', 7}, // U+0024 DOLLAR SIGN
|
|
{'\u0025', 8}, // U+0025 PERCENT SIGN
|
|
{'\u0026', 9}, // U+0026 AMPERSAND
|
|
{'\u0027', 10}, // U+0027 APOSTROPHE
|
|
|
|
{'\u03bd', 396}, // U+03BD GREEK SMALL LETTER NU
|
|
{'\u03be', 397}, // U+03BE GREEK SMALL LETTER XI
|
|
{'\u03bf', 398}, // U+03BF GREEK SMALL LETTER OMICRON
|
|
{'\u03c0', 399}, // U+03C0 GREEK SMALL LETTER PI
|
|
{'\u03c1', 400}, // U+03C1 GREEK SMALL LETTER RHO
|
|
{'\u03c2', 401}, // U+03C2 GREEK SMALL LETTER FINAL SIGMA
|
|
}
|
|
|
|
var b Buffer
|
|
for _, tc := range testCases {
|
|
got, err := f.GlyphIndex(&b, tc.r)
|
|
if err != nil {
|
|
t.Errorf("r=%q: %v", tc.r, err)
|
|
continue
|
|
}
|
|
if got != tc.want {
|
|
t.Errorf("r=%q: got %d, want %d", tc.r, got, tc.want)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGlyphIndex(t *testing.T) {
|
|
data, err := ioutil.ReadFile(filepath.FromSlash("../testdata/cmapTest.ttf"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for _, format := range []int{-1, 0, 4, 12} {
|
|
testGlyphIndex(t, data, format)
|
|
}
|
|
}
|
|
|
|
func testGlyphIndex(t *testing.T, data []byte, cmapFormat int) {
|
|
if cmapFormat >= 0 {
|
|
originalSupportedCmapFormat := supportedCmapFormat
|
|
defer func() {
|
|
supportedCmapFormat = originalSupportedCmapFormat
|
|
}()
|
|
supportedCmapFormat = func(format, pid, psid uint16) bool {
|
|
return int(format) == cmapFormat && originalSupportedCmapFormat(format, pid, psid)
|
|
}
|
|
}
|
|
|
|
f, err := Parse(data)
|
|
if err != nil {
|
|
t.Errorf("cmapFormat=%d: %v", cmapFormat, err)
|
|
return
|
|
}
|
|
|
|
testCases := []struct {
|
|
r rune
|
|
want GlyphIndex
|
|
}{
|
|
// Glyphs that aren't present in cmapTest.ttf.
|
|
{'?', 0},
|
|
{'\ufffd', 0},
|
|
{'\U0001f4a9', 0},
|
|
|
|
// For a .TTF file, FontForge maps:
|
|
// - ".notdef" to glyph index 0.
|
|
// - ".null" to glyph index 1.
|
|
// - "nonmarkingreturn" to glyph index 2.
|
|
|
|
{'/', 0},
|
|
{'0', 3},
|
|
{'1', 4},
|
|
{'2', 5},
|
|
{'3', 0},
|
|
|
|
{'@', 0},
|
|
{'A', 6},
|
|
{'B', 7},
|
|
{'C', 0},
|
|
|
|
{'`', 0},
|
|
{'a', 8},
|
|
{'b', 0},
|
|
|
|
// Of the remaining runes, only U+00FF LATIN SMALL LETTER Y WITH
|
|
// DIAERESIS is in both the Mac Roman encoding and the cmapTest.ttf
|
|
// font file.
|
|
{'\u00fe', 0},
|
|
{'\u00ff', 9},
|
|
{'\u0100', 10},
|
|
{'\u0101', 11},
|
|
{'\u0102', 0},
|
|
|
|
{'\u4e2c', 0},
|
|
{'\u4e2d', 12},
|
|
{'\u4e2e', 0},
|
|
|
|
{'\U0001f0a0', 0},
|
|
{'\U0001f0a1', 13},
|
|
{'\U0001f0a2', 0},
|
|
|
|
{'\U0001f0b0', 0},
|
|
{'\U0001f0b1', 14},
|
|
{'\U0001f0b2', 15},
|
|
{'\U0001f0b3', 0},
|
|
}
|
|
|
|
var b Buffer
|
|
for _, tc := range testCases {
|
|
want := tc.want
|
|
switch {
|
|
case cmapFormat == 0 && tc.r > '\u007f' && tc.r != '\u00ff':
|
|
// cmap format 0, with the Macintosh Roman encoding, can only
|
|
// represent a limited set of non-ASCII runes, e.g. U+00FF.
|
|
want = 0
|
|
case cmapFormat == 4 && tc.r > '\uffff':
|
|
// cmap format 4 only supports the Basic Multilingual Plane (BMP).
|
|
want = 0
|
|
}
|
|
|
|
got, err := f.GlyphIndex(&b, tc.r)
|
|
if err != nil {
|
|
t.Errorf("cmapFormat=%d, r=%q: %v", cmapFormat, tc.r, err)
|
|
continue
|
|
}
|
|
if got != want {
|
|
t.Errorf("cmapFormat=%d, r=%q: got %d, want %d", cmapFormat, tc.r, got, want)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
// drawn in a consistent direction. Clockwise for external paths,
|
|
// anti-clockwise for internal paths. (Actually PostScript requires the
|
|
// exact opposite, but FontForge reverses PostScript contours when it loads
|
|
// them so that everything is consistant internally -- and reverses them
|
|
// again when it saves them, of course)."
|
|
//
|
|
// The .notdef glyph isn't explicitly in the SFD file, but for some unknown
|
|
// reason, FontForge generates it in the OpenType/CFF file.
|
|
wants := [][]Segment{{
|
|
// .notdef
|
|
// - contour #0
|
|
moveTo(50, 0),
|
|
lineTo(450, 0),
|
|
lineTo(450, 533),
|
|
lineTo(50, 533),
|
|
lineTo(50, 0),
|
|
// - contour #1
|
|
moveTo(100, 50),
|
|
lineTo(100, 483),
|
|
lineTo(400, 483),
|
|
lineTo(400, 50),
|
|
lineTo(100, 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),
|
|
lineTo(100, 0),
|
|
}, {
|
|
// Q
|
|
// - contour #0
|
|
moveTo(657, 237),
|
|
lineTo(289, 387),
|
|
lineTo(519, 615),
|
|
lineTo(657, 237),
|
|
// - contour #1
|
|
moveTo(792, 169),
|
|
cubeTo(867, 263, 926, 502, 791, 665),
|
|
cubeTo(645, 840, 380, 831, 228, 673),
|
|
cubeTo(71, 509, 110, 231, 242, 93),
|
|
cubeTo(369, -39, 641, 18, 722, 93),
|
|
lineTo(802, 3),
|
|
lineTo(864, 83),
|
|
lineTo(792, 169),
|
|
}, {
|
|
// 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),
|
|
lineTo(141, 520),
|
|
}}
|
|
|
|
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),
|
|
}, {
|
|
// five
|
|
// - contour #0
|
|
moveTo(0, 0),
|
|
lineTo(0, 100),
|
|
lineTo(400, 100),
|
|
lineTo(400, 0),
|
|
lineTo(0, 0),
|
|
}, {
|
|
// six
|
|
// - contour #0
|
|
moveTo(0, 0),
|
|
lineTo(0, 100),
|
|
lineTo(400, 100),
|
|
lineTo(400, 0),
|
|
lineTo(0, 0),
|
|
// - contour #1
|
|
translate(111, 234, moveTo(205, 0)),
|
|
translate(111, 234, lineTo(205, 1638)),
|
|
translate(111, 234, lineTo(614, 1638)),
|
|
translate(111, 234, lineTo(614, 0)),
|
|
translate(111, 234, lineTo(205, 0)),
|
|
}, {
|
|
// seven
|
|
// - contour #0
|
|
moveTo(0, 0),
|
|
lineTo(0, 100),
|
|
lineTo(400, 100),
|
|
lineTo(400, 0),
|
|
lineTo(0, 0),
|
|
// - contour #1
|
|
transform(1<<13, 0, 0, 1<<13, 56, 117, moveTo(205, 0)),
|
|
transform(1<<13, 0, 0, 1<<13, 56, 117, lineTo(205, 1638)),
|
|
transform(1<<13, 0, 0, 1<<13, 56, 117, lineTo(614, 1638)),
|
|
transform(1<<13, 0, 0, 1<<13, 56, 117, lineTo(614, 0)),
|
|
transform(1<<13, 0, 0, 1<<13, 56, 117, lineTo(205, 0)),
|
|
}, {
|
|
// eight
|
|
// - contour #0
|
|
moveTo(0, 0),
|
|
lineTo(0, 100),
|
|
lineTo(400, 100),
|
|
lineTo(400, 0),
|
|
lineTo(0, 0),
|
|
// - contour #1
|
|
transform(3<<13, 0, 0, 1<<13, 56, 117, moveTo(205, 0)),
|
|
transform(3<<13, 0, 0, 1<<13, 56, 117, lineTo(205, 1638)),
|
|
transform(3<<13, 0, 0, 1<<13, 56, 117, lineTo(614, 1638)),
|
|
transform(3<<13, 0, 0, 1<<13, 56, 117, lineTo(614, 0)),
|
|
transform(3<<13, 0, 0, 1<<13, 56, 117, lineTo(205, 0)),
|
|
}, {
|
|
// nine
|
|
// - contour #0
|
|
moveTo(0, 0),
|
|
lineTo(0, 100),
|
|
lineTo(400, 100),
|
|
lineTo(400, 0),
|
|
lineTo(0, 0),
|
|
// - contour #1
|
|
transform(22381, 8192, 5996, 14188, 237, 258, moveTo(205, 0)),
|
|
transform(22381, 8192, 5996, 14188, 237, 258, lineTo(205, 1638)),
|
|
transform(22381, 8192, 5996, 14188, 237, 258, lineTo(614, 1638)),
|
|
transform(22381, 8192, 5996, 14188, 237, 258, lineTo(614, 0)),
|
|
transform(22381, 8192, 5996, 14188, 237, 258, lineTo(205, 0)),
|
|
}}
|
|
|
|
testSegments(t, "glyfTest.ttf", wants)
|
|
}
|
|
|
|
func testSegments(t *testing.T, filename string, wants [][]Segment) {
|
|
data, err := ioutil.ReadFile(filepath.FromSlash("../testdata/" + filename))
|
|
if err != nil {
|
|
t.Fatalf("ReadFile: %v", err)
|
|
}
|
|
f, err := Parse(data)
|
|
if err != nil {
|
|
t.Fatalf("Parse: %v", err)
|
|
}
|
|
ppem := fixed.Int26_6(f.UnitsPerEm())
|
|
|
|
if ng := f.NumGlyphs(); ng != len(wants) {
|
|
t.Fatalf("NumGlyphs: got %d, want %d", ng, len(wants))
|
|
}
|
|
var b Buffer
|
|
for i, want := range wants {
|
|
got, err := f.LoadGlyph(&b, GlyphIndex(i), ppem, nil)
|
|
if err != nil {
|
|
t.Errorf("i=%d: LoadGlyph: %v", i, err)
|
|
continue
|
|
}
|
|
if err := checkSegmentsEqual(got, want); err != nil {
|
|
t.Errorf("i=%d: %v", i, err)
|
|
continue
|
|
}
|
|
}
|
|
if _, err := f.LoadGlyph(nil, 0xffff, ppem, nil); err != ErrNotFound {
|
|
t.Errorf("LoadGlyph(..., 0xffff, ...):\ngot %v\nwant %v", err, ErrNotFound)
|
|
}
|
|
|
|
name, err := f.Name(nil, NameIDFamily)
|
|
if err != nil {
|
|
t.Errorf("Name: %v", err)
|
|
} else if want := filename[:len(filename)-len(".ttf")]; name != want {
|
|
t.Errorf("Name:\ngot %q\nwant %q", name, want)
|
|
}
|
|
}
|
|
|
|
func TestPPEM(t *testing.T) {
|
|
data, err := ioutil.ReadFile(filepath.FromSlash("../testdata/glyfTest.ttf"))
|
|
if err != nil {
|
|
t.Fatalf("ReadFile: %v", err)
|
|
}
|
|
f, err := Parse(data)
|
|
if err != nil {
|
|
t.Fatalf("Parse: %v", err)
|
|
}
|
|
var b Buffer
|
|
x, err := f.GlyphIndex(&b, '1')
|
|
if err != nil {
|
|
t.Fatalf("GlyphIndex: %v", err)
|
|
}
|
|
if x == 0 {
|
|
t.Fatalf("GlyphIndex: no glyph index found for the rune '1'")
|
|
}
|
|
|
|
testCases := []struct {
|
|
ppem fixed.Int26_6
|
|
want []Segment
|
|
}{{
|
|
ppem: fixed.Int26_6(12 << 6),
|
|
want: []Segment{
|
|
moveTo(77, 0),
|
|
lineTo(77, 614),
|
|
lineTo(230, 614),
|
|
lineTo(230, 0),
|
|
lineTo(77, 0),
|
|
},
|
|
}, {
|
|
ppem: fixed.Int26_6(2048),
|
|
want: []Segment{
|
|
moveTo(205, 0),
|
|
lineTo(205, 1638),
|
|
lineTo(614, 1638),
|
|
lineTo(614, 0),
|
|
lineTo(205, 0),
|
|
},
|
|
}}
|
|
|
|
for i, tc := range testCases {
|
|
got, err := f.LoadGlyph(&b, x, tc.ppem, nil)
|
|
if err != nil {
|
|
t.Errorf("i=%d: LoadGlyph: %v", i, err)
|
|
continue
|
|
}
|
|
if err := checkSegmentsEqual(got, tc.want); err != nil {
|
|
t.Errorf("i=%d: %v", i, err)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPostInfo(t *testing.T) {
|
|
data, err := ioutil.ReadFile(filepath.FromSlash("../testdata/glyfTest.ttf"))
|
|
if err != nil {
|
|
t.Fatalf("ReadFile: %v", err)
|
|
}
|
|
f, err := Parse(data)
|
|
if err != nil {
|
|
t.Fatalf("Parse: %v", err)
|
|
}
|
|
post := f.PostTable()
|
|
if post.ItalicAngle != -11.25 {
|
|
t.Error("ItalicAngle:", post.ItalicAngle)
|
|
}
|
|
if post.UnderlinePosition != -255 {
|
|
t.Error("UnderlinePosition:", post.UnderlinePosition)
|
|
}
|
|
if post.UnderlineThickness != 102 {
|
|
t.Error("UnderlineThickness:", post.UnderlineThickness)
|
|
}
|
|
if post.IsFixedPitch {
|
|
t.Error("IsFixedPitch:", post.IsFixedPitch)
|
|
}
|
|
}
|
|
|
|
func TestGlyphName(t *testing.T) {
|
|
f, err := Parse(goregular.TTF)
|
|
if err != nil {
|
|
t.Fatalf("Parse: %v", err)
|
|
}
|
|
|
|
testCases := []struct {
|
|
r rune
|
|
want string
|
|
}{
|
|
{'\x00', "uni0000"},
|
|
{'!', "exclam"},
|
|
{'A', "A"},
|
|
{'{', "braceleft"},
|
|
{'\u00c4', "Adieresis"}, // U+00C4 LATIN CAPITAL LETTER A WITH DIAERESIS
|
|
{'\u2020', "dagger"}, // U+2020 DAGGER
|
|
{'\u2660', "spade"}, // U+2660 BLACK SPADE SUIT
|
|
{'\uf800', "gopher"}, // U+F800 <Private Use>
|
|
{'\ufffe', ".notdef"}, // Not in the Go Regular font, so GlyphIndex returns (0, nil).
|
|
}
|
|
|
|
var b Buffer
|
|
for _, tc := range testCases {
|
|
x, err := f.GlyphIndex(&b, tc.r)
|
|
if err != nil {
|
|
t.Errorf("r=%q: GlyphIndex: %v", tc.r, err)
|
|
continue
|
|
}
|
|
got, err := f.GlyphName(&b, x)
|
|
if err != nil {
|
|
t.Errorf("r=%q: GlyphName: %v", tc.r, err)
|
|
continue
|
|
}
|
|
if got != tc.want {
|
|
t.Errorf("r=%q: got %q, want %q", tc.r, got, tc.want)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuiltInPostNames(t *testing.T) {
|
|
testCases := []struct {
|
|
x GlyphIndex
|
|
want string
|
|
}{
|
|
{0, ".notdef"},
|
|
{1, ".null"},
|
|
{2, "nonmarkingreturn"},
|
|
{13, "asterisk"},
|
|
{36, "A"},
|
|
{93, "z"},
|
|
{123, "ocircumflex"},
|
|
{202, "Edieresis"},
|
|
{255, "Ccaron"},
|
|
{256, "ccaron"},
|
|
{257, "dcroat"},
|
|
{258, ""},
|
|
{999, ""},
|
|
{0xffff, ""},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
if tc.x >= numBuiltInPostNames {
|
|
continue
|
|
}
|
|
i := builtInPostNamesOffsets[tc.x+0]
|
|
j := builtInPostNamesOffsets[tc.x+1]
|
|
got := builtInPostNamesData[i:j]
|
|
if got != tc.want {
|
|
t.Errorf("x=%d: got %q, want %q", tc.x, got, tc.want)
|
|
}
|
|
}
|
|
}
|