From bc291dd37a438fcb9eeddd5ce058f1353be7c4ba Mon Sep 17 00:00:00 2001 From: Fazlul Shahriar Date: Tue, 2 Apr 2019 18:55:54 -0400 Subject: [PATCH] Replace freetype with sfnt Fixes #17 --- font.go | 41 ++++---- font_test.go | 19 +++- internal/opentype/face.go | 174 ++++++++++++++++++++++++++++++++++ internal/opentype/opentype.go | 7 ++ 4 files changed, 218 insertions(+), 23 deletions(-) create mode 100644 internal/opentype/face.go create mode 100644 internal/opentype/opentype.go diff --git a/font.go b/font.go index 4b20113..15dc840 100644 --- a/font.go +++ b/font.go @@ -9,10 +9,11 @@ import ( "strings" "sync" - "github.com/golang/freetype/truetype" + "github.com/ktye/duitdraw/internal/opentype" "golang.org/x/image/font" "golang.org/x/image/font/gofont/goregular" "golang.org/x/image/font/plan9font" + "golang.org/x/image/font/sfnt" "golang.org/x/image/math/fixed" ) @@ -71,11 +72,9 @@ func openFont(id FaceID) (*Font, error) { faceCache.Lock() defer faceCache.Unlock() if f, ok := faceCache.m[id]; ok { - m := f.Metrics() - // TODO(fhs): Remove workaround for wrong m.Height. return &Font{ FaceID: id, - Height: (m.Ascent + m.Descent).Round(), + Height: f.Metrics().Height.Round(), face: f, }, nil } @@ -91,34 +90,26 @@ func openFont(id FaceID) (*Font, error) { } } - if f, err := truetype.Parse(ttf); err != nil { + if f, err := sfnt.Parse(ttf); err != nil { return nil, fmt.Errorf("%s: %s", id.Name, err) } else { - opt := truetype.Options{ - Size: float64(id.Size), - DPI: float64(id.DPI), + ff, err := opentype.NewFace(f, &opentype.FaceOptions{ + Size: float64(id.Size), + DPI: float64(id.DPI), + Hinting: font.HintingNone, + }) + if err != nil { + return nil, err } - face := pixFace{Face: truetype.NewFace(f, &opt)} + face := pixFace{Face: ff} faceCache.m[id] = face - m := face.Metrics() - // TODO(fhs): Remove workaround for wrong m.Height. return &Font{ FaceID: id, - Height: (m.Ascent + m.Descent).Round(), + Height: face.Metrics().Height.Round(), face: face, }, nil } - - /* TODO: use sfnt/opentype, when it's finished. - f, err := sfnt.Parse(ttf) - opt := opentype.FaceOptions{ - Size: 12, - DPI: 72, - Hinting: font.HintingNone, - } - face, err := opentype.NewFace(f, &opt) - */ } func openPlan9Font(id FaceID) (*Font, error) { @@ -215,6 +206,12 @@ func (f pixFace) GlyphBounds(r rune) (bounds fixed.Rectangle26_6, advance fixed. return } +func (f pixFace) GlyphAdvance(r rune) (advance fixed.Int26_6, ok bool) { + advance, ok = f.Face.GlyphAdvance(r) + advance = 64 * fixed.Int26_6(int(advance+32)/64) + return +} + // defaultFont is used for new Displays. // It is GoRegular at DefaultSize for DefaultDPI. var defaultFont *Font diff --git a/font_test.go b/font_test.go index 3926e2c..17024bc 100644 --- a/font_test.go +++ b/font_test.go @@ -1,6 +1,9 @@ package duitdraw -import "testing" +import ( + "image" + "testing" +) func TestStringWidth(t *testing.T) { tt := []string{ @@ -20,3 +23,17 @@ func TestStringWidth(t *testing.T) { } } } + +func TestImageString(t *testing.T) { + d, err := Init(nil, "", "Image String test", "") + if err != nil { + t.Fatalf("can't open display: %v", err) + } + defer d.Close() + + dst := d.MakeImage(image.NewRGBA(image.Rect(0, 0, 100, 100))) + p := dst.String(image.ZP, d.Black, image.ZP, defaultFont, "Hello, 世界") + if p.X <= 0 || p.Y != 0 { + t.Errorf("String returned bad point %v", p) + } +} diff --git a/internal/opentype/face.go b/internal/opentype/face.go new file mode 100644 index 0000000..05715a7 --- /dev/null +++ b/internal/opentype/face.go @@ -0,0 +1,174 @@ +// 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 opentype + +import ( + "image" + "image/draw" + + "golang.org/x/image/font" + "golang.org/x/image/font/sfnt" + "golang.org/x/image/math/fixed" + "golang.org/x/image/vector" +) + +// FaceOptions describes the possible options given to NewFace when +// creating a new font.Face from a sfnt.Font. +type FaceOptions struct { + Size float64 // Size is the font size in points + DPI float64 // DPI is the dots per inch resolution + Hinting font.Hinting // Hinting selects how to quantize a vector font's glyph nodes +} + +func defaultFaceOptions() *FaceOptions { + return &FaceOptions{ + Size: 12, + DPI: 72, + Hinting: font.HintingNone, + } +} + +// Face implements the font.Face interface for sfnt.Font values. +type Face struct { + f *sfnt.Font + hinting font.Hinting + scale fixed.Int26_6 + + buf sfnt.Buffer +} + +// NewFace returns a new font.Face for the given sfnt.Font. +// if opts is nil, sensible defaults will be used. +func NewFace(f *sfnt.Font, opts *FaceOptions) (font.Face, error) { + if opts == nil { + opts = defaultFaceOptions() + } + face := &Face{ + f: f, + hinting: opts.Hinting, + scale: fixed.Int26_6(0.5 + (opts.Size * opts.DPI * 64 / 72)), + } + return face, nil +} + +// Close satisfies the font.Face interface. +func (f *Face) Close() error { + return nil +} + +// Metrics satisfies the font.Face interface. +func (f *Face) Metrics() font.Metrics { + m, err := f.f.Metrics(&f.buf, f.scale, f.hinting) + if err != nil { + return font.Metrics{} + } + return m +} + +// Kern satisfies the font.Face interface. +func (f *Face) Kern(r0, r1 rune) fixed.Int26_6 { + x0 := f.index(r0) + x1 := f.index(r1) + k, err := f.f.Kern(&f.buf, x0, x1, fixed.Int26_6(f.f.UnitsPerEm()), f.hinting) + if err != nil { + return 0 + } + return k +} + +// Glyph satisfies the font.Face interface. +func (f *Face) Glyph(dot fixed.Point26_6, r rune) (dr image.Rectangle, mask image.Image, maskp image.Point, advance fixed.Int26_6, ok bool) { + m := f.Metrics() + height := m.Height.Round() + advance, _ = f.GlyphAdvance(r) + width := advance.Round() + originX := float32(0) + originY := float32(m.Ascent.Round()) + + var b sfnt.Buffer + x, err := f.f.GlyphIndex(&b, r) + if err != nil { + ok = false + return + } + segments, err := f.f.LoadGlyph(&b, x, f.scale, nil) + if err != nil { + ok = false + return + } + + rast := vector.NewRasterizer(width, height) + rast.DrawOp = draw.Src + for _, seg := range segments { + // The divisions by 64 below is because the seg.Args values have type + // fixed.Int26_6, a 26.6 fixed point number, and 1<<6 == 64. + switch seg.Op { + case sfnt.SegmentOpMoveTo: + rast.MoveTo( + originX+float32(seg.Args[0].X)/64, + originY+float32(seg.Args[0].Y)/64, + ) + case sfnt.SegmentOpLineTo: + rast.LineTo( + originX+float32(seg.Args[0].X)/64, + originY+float32(seg.Args[0].Y)/64, + ) + case sfnt.SegmentOpQuadTo: + rast.QuadTo( + originX+float32(seg.Args[0].X)/64, + originY+float32(seg.Args[0].Y)/64, + originX+float32(seg.Args[1].X)/64, + originY+float32(seg.Args[1].Y)/64, + ) + case sfnt.SegmentOpCubeTo: + rast.CubeTo( + originX+float32(seg.Args[0].X)/64, + originY+float32(seg.Args[0].Y)/64, + originX+float32(seg.Args[1].X)/64, + originY+float32(seg.Args[1].Y)/64, + originX+float32(seg.Args[2].X)/64, + originY+float32(seg.Args[2].Y)/64, + ) + } + } + + dst := image.NewAlpha(image.Rect(0, 0, width, height)) + rast.Draw(dst, dst.Bounds(), image.Opaque, image.ZP) + + dr = image.Rectangle{ + Min: image.Point{ + X: dst.Rect.Min.X + dot.X.Round(), + Y: dst.Rect.Min.Y + dot.Y.Round() - m.Ascent.Round(), + }, + Max: image.Point{ + X: dst.Rect.Max.X + dot.X.Round(), + Y: dst.Rect.Max.Y + dot.Y.Round() - m.Ascent.Round(), + }, + } + return dr, dst, image.ZP, advance, true +} + +// GlyphBounds satisfies the font.Face interface. +func (f *Face) GlyphBounds(r rune) (bounds fixed.Rectangle26_6, advance fixed.Int26_6, ok bool) { + advance, ok = f.GlyphAdvance(r) + if !ok { + return bounds, advance, ok + } + var err error + bounds, err = f.f.Bounds(&f.buf, f.scale, f.hinting) + return bounds, advance, err == nil +} + +// GlyphAdvance satisfies the font.Face interface. +func (f *Face) GlyphAdvance(r rune) (advance fixed.Int26_6, ok bool) { + idx := f.index(r) + advance, err := f.f.GlyphAdvance(&f.buf, idx, f.scale, f.hinting) + return advance, err == nil +} + +func (f *Face) index(r rune) sfnt.GlyphIndex { + x, _ := f.f.GlyphIndex(&f.buf, r) + return x +} diff --git a/internal/opentype/opentype.go b/internal/opentype/opentype.go new file mode 100644 index 0000000..18d7857 --- /dev/null +++ b/internal/opentype/opentype.go @@ -0,0 +1,7 @@ +// 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 opentype implements the font.Face interface based on SFNT +// font file formats. +package opentype