diff --git a/font/gofont/gofont.go b/font/gofont/gofont.go index 7c7555d0..104a83d8 100644 --- a/font/gofont/gofont.go +++ b/font/gofont/gofont.go @@ -78,7 +78,7 @@ func CollectionHB() []text.FontFace { } func registerHB(fnt text.Font, ttf []byte) { - face, err := opentype.ParseHarfbuzz(ttf) + face, err := opentype.Parse(ttf) if err != nil { panic(fmt.Errorf("failed to parse font: %v", err)) } diff --git a/font/opentype/opentype.go b/font/opentype/opentype.go index bb8798a9..87d76507 100644 --- a/font/opentype/opentype.go +++ b/font/opentype/opentype.go @@ -9,15 +9,12 @@ import ( "fmt" "image" "io" - "unicode" - "unicode/utf8" "github.com/benoitkugler/textlayout/fonts" "github.com/benoitkugler/textlayout/fonts/truetype" "github.com/benoitkugler/textlayout/harfbuzz" "github.com/go-text/typesetting/shaping" "golang.org/x/image/font" - "golang.org/x/image/font/sfnt" "golang.org/x/image/math/fixed" "gioui.org/f32" @@ -28,32 +25,32 @@ import ( "gioui.org/text" ) -// HarfbuzzFont implements the text.Shaper interface using a rich text +// Font implements the text.Shaper interface using a rich text // shaping engine. -type HarfbuzzFont struct { +type Font struct { font *truetype.Font } -// ParseHarfbuzz constructs a HarfbuzzFont from source bytes. -func ParseHarfbuzz(src []byte) (*HarfbuzzFont, error) { +// Parse constructs a Font from source bytes. +func Parse(src []byte) (*Font, error) { face, err := truetype.Parse(bytes.NewReader(src)) if err != nil { return nil, fmt.Errorf("failed parsing truetype font: %w", err) } - return &HarfbuzzFont{ + return &Font{ font: face, }, nil } -func (f *HarfbuzzFont) Layout(ppem fixed.Int26_6, maxWidth int, lc system.Locale, txt io.RuneReader) ([]text.Line, error) { +func (f *Font) Layout(ppem fixed.Int26_6, maxWidth int, lc system.Locale, txt io.RuneReader) ([]text.Line, error) { return internal.Document(shaping.Shape, f.font, ppem, maxWidth, lc, txt), nil } -func (f *HarfbuzzFont) Shape(ppem fixed.Int26_6, str text.Layout) clip.PathSpec { - return harfbuzzTextPath(ppem, f, str) +func (f *Font) Shape(ppem fixed.Int26_6, str text.Layout) clip.PathSpec { + return textPath(ppem, f, str) } -func (f *HarfbuzzFont) Metrics(ppem fixed.Int26_6) font.Metrics { +func (f *Font) Metrics(ppem fixed.Int26_6) font.Metrics { metrics := font.Metrics{} font := harfbuzz.NewFont(f.font) font.XScale = int32(ppem.Ceil()) << 6 @@ -75,312 +72,7 @@ func (f *HarfbuzzFont) Metrics(ppem fixed.Int26_6) font.Metrics { return metrics } -// Font implements text.Face. Its methods are safe to use -// concurrently. -type Font struct { - font *sfnt.Font -} - -// Collection is a collection of one or more fonts. When used as a text.Face, -// each rune will be assigned a glyph from the first font in the collection -// that supports it. -type Collection struct { - fonts []*opentype -} - -type opentype struct { - Font *sfnt.Font - Hinting font.Hinting -} - -// a glyph represents a rune and its advance according to a Font. -// TODO: remove this type and work on io.Readers directly. -type glyph struct { - Rune rune - Advance fixed.Int26_6 -} - -// NewFont parses an SFNT font, such as TTF or OTF data, from a []byte -// data source. -func Parse(src []byte) (*Font, error) { - fnt, err := sfnt.Parse(src) - if err != nil { - return nil, err - } - return &Font{font: fnt}, nil -} - -// ParseCollection parses an SFNT font collection, such as TTC or OTC data, -// from a []byte data source. -// -// If passed data for a single font, a TTF or OTF instead of a TTC or OTC, -// it will return a collection containing 1 font. -func ParseCollection(src []byte) (*Collection, error) { - c, err := sfnt.ParseCollection(src) - if err != nil { - return nil, err - } - return newCollectionFrom(c) -} - -// ParseCollectionReaderAt parses an SFNT collection, such as TTC or OTC data, -// from an io.ReaderAt data source. -// -// If passed data for a single font, a TTF or OTF instead of a TTC or OTC, it -// will return a collection containing 1 font. -func ParseCollectionReaderAt(src io.ReaderAt) (*Collection, error) { - c, err := sfnt.ParseCollectionReaderAt(src) - if err != nil { - return nil, err - } - return newCollectionFrom(c) -} - -func newCollectionFrom(coll *sfnt.Collection) (*Collection, error) { - fonts := make([]*opentype, coll.NumFonts()) - for i := range fonts { - fnt, err := coll.Font(i) - if err != nil { - return nil, err - } - fonts[i] = &opentype{ - Font: fnt, - Hinting: font.HintingFull, - } - } - return &Collection{fonts: fonts}, nil -} - -// NumFonts returns the number of fonts in the collection. -func (c *Collection) NumFonts() int { - return len(c.fonts) -} - -// Font returns the i'th font in the collection. -func (c *Collection) Font(i int) (*Font, error) { - if i < 0 || len(c.fonts) <= i { - return nil, sfnt.ErrNotFound - } - return &Font{font: c.fonts[i].Font}, nil -} - -func (f *Font) Layout(ppem fixed.Int26_6, maxWidth int, lc system.Locale, txt io.RuneReader) ([]text.Line, error) { - glyphs, err := readGlyphs(txt) - if err != nil { - return nil, err - } - fonts := []*opentype{{Font: f.font, Hinting: font.HintingFull}} - var buf sfnt.Buffer - return layoutText(&buf, ppem, maxWidth, fonts, glyphs) -} - -func (f *Font) Shape(ppem fixed.Int26_6, str text.Layout) clip.PathSpec { - var buf sfnt.Buffer - return textPath(&buf, ppem, []*opentype{{Font: f.font, Hinting: font.HintingFull}}, str) -} - -func (f *Font) Metrics(ppem fixed.Int26_6) font.Metrics { - o := &opentype{Font: f.font, Hinting: font.HintingFull} - var buf sfnt.Buffer - return o.Metrics(&buf, ppem) -} - -func (c *Collection) Layout(ppem fixed.Int26_6, maxWidth int, lc system.Locale, txt io.RuneReader) ([]text.Line, error) { - glyphs, err := readGlyphs(txt) - if err != nil { - return nil, err - } - var buf sfnt.Buffer - return layoutText(&buf, ppem, maxWidth, c.fonts, glyphs) -} - -func (c *Collection) Shape(ppem fixed.Int26_6, str text.Layout) clip.PathSpec { - var buf sfnt.Buffer - return textPath(&buf, ppem, c.fonts, str) -} - -func fontForGlyph(buf *sfnt.Buffer, fonts []*opentype, r rune) *opentype { - if len(fonts) < 1 { - return nil - } - for _, f := range fonts { - if f.HasGlyph(buf, r) { - return f - } - } - return fonts[0] // Use replacement character from the first font if necessary -} - -func layoutText(sbuf *sfnt.Buffer, ppem fixed.Int26_6, maxWidth int, fonts []*opentype, glyphs []glyph) ([]text.Line, error) { - var lines []text.Line - var nextLine text.Line - updateBounds := func(f *opentype) { - m := f.Metrics(sbuf, ppem) - if m.Ascent > nextLine.Ascent { - nextLine.Ascent = m.Ascent - } - // m.Height is equal to m.Ascent + m.Descent + linegap. - // Compute the descent including the linegap. - descent := m.Height - m.Ascent - if descent > nextLine.Descent { - nextLine.Descent = descent - } - b := f.Bounds(sbuf, ppem) - nextLine.Bounds = nextLine.Bounds.Union(b) - } - maxDotX := fixed.I(maxWidth) - type state struct { - r rune - f *opentype - adv fixed.Int26_6 - x fixed.Int26_6 - idx int - len int - valid bool - } - var prev, word state - endLine := func() { - if prev.f == nil && len(fonts) > 0 { - prev.f = fonts[0] - } - updateBounds(prev.f) - nextLine.Layout = toLayout(glyphs[:prev.idx:prev.idx]) - nextLine.Width = prev.x + prev.adv - nextLine.Bounds.Max.X += prev.x - lines = append(lines, nextLine) - glyphs = glyphs[prev.idx:] - nextLine = text.Line{} - prev = state{} - word = state{} - } - for prev.idx < len(glyphs) { - g := &glyphs[prev.idx] - next := state{ - r: g.Rune, - f: fontForGlyph(sbuf, fonts, g.Rune), - idx: prev.idx + 1, - len: prev.len + utf8.RuneLen(g.Rune), - x: prev.x + prev.adv, - } - if next.f != nil { - if next.f != prev.f { - updateBounds(next.f) - } - next.adv, next.valid = next.f.GlyphAdvance(sbuf, ppem, g.Rune) - } - if g.Rune == '\n' { - // The newline is zero width; use the previous - // character for line measurements. - prev.idx = next.idx - prev.len = next.len - endLine() - continue - } - var k fixed.Int26_6 - if prev.valid && next.f != nil { - k = next.f.Kern(sbuf, ppem, prev.r, next.r) - } - // Break the line if we're out of space. - if prev.idx > 0 && next.x+next.adv+k > maxDotX { - // If the line contains no word breaks, break off the last rune. - if word.idx == 0 { - word = prev - } - next.x -= word.x + word.adv - next.idx -= word.idx - next.len -= word.len - prev = word - endLine() - } else if k != 0 { - glyphs[prev.idx-1].Advance += k - next.x += k - } - g.Advance = next.adv - if unicode.IsSpace(g.Rune) { - word = next - } - prev = next - } - endLine() - return lines, nil -} - -// toLayout converts a slice of glyphs to a text.Layout. -func toLayout(glyphs []glyph) text.Layout { - var buf bytes.Buffer - advs := make([]fixed.Int26_6, len(glyphs)) - for i, g := range glyphs { - buf.WriteRune(g.Rune) - advs[i] = glyphs[i].Advance - } - return text.Layout{Text: buf.String(), Advances: advs} -} - -func textPath(buf *sfnt.Buffer, ppem fixed.Int26_6, fonts []*opentype, str text.Layout) clip.PathSpec { - var lastPos f32.Point - var builder clip.Path - var x fixed.Int26_6 - builder.Begin(new(op.Ops)) - rune := 0 - for _, r := range str.Text { - if !unicode.IsSpace(r) { - f := fontForGlyph(buf, fonts, r) - if f == nil { - continue - } - segs, ok := f.LoadGlyph(buf, ppem, r) - if !ok { - continue - } - // Move to glyph position. - pos := f32.Point{ - X: float32(x) / 64, - } - builder.Move(pos.Sub(lastPos)) - lastPos = pos - var lastArg f32.Point - // Convert sfnt.Segments to relative segments. - for _, fseg := range segs { - nargs := 1 - switch fseg.Op { - case sfnt.SegmentOpQuadTo: - nargs = 2 - case sfnt.SegmentOpCubeTo: - nargs = 3 - } - var args [3]f32.Point - for i := 0; i < nargs; i++ { - a := f32.Point{ - X: float32(fseg.Args[i].X) / 64, - Y: float32(fseg.Args[i].Y) / 64, - } - args[i] = a.Sub(lastArg) - if i == nargs-1 { - lastArg = a - } - } - switch fseg.Op { - case sfnt.SegmentOpMoveTo: - builder.Move(args[0]) - case sfnt.SegmentOpLineTo: - builder.Line(args[0]) - case sfnt.SegmentOpQuadTo: - builder.Quad(args[0], args[1]) - case sfnt.SegmentOpCubeTo: - builder.Cube(args[0], args[1], args[2]) - default: - panic("unsupported segment op") - } - } - lastPos = lastPos.Add(lastArg) - } - x += str.Advances[rune] - rune++ - } - return builder.End() -} - -func harfbuzzTextPath(ppem fixed.Int26_6, font *HarfbuzzFont, str text.Layout) clip.PathSpec { +func textPath(ppem fixed.Int26_6, font *Font, str text.Layout) clip.PathSpec { var lastPos f32.Point var builder clip.Path ops := new(op.Ops) @@ -444,70 +136,3 @@ func harfbuzzTextPath(ppem fixed.Int26_6, font *HarfbuzzFont, str text.Layout) c } return builder.End() } - -func readGlyphs(r io.RuneReader) ([]glyph, error) { - var glyphs []glyph - for { - c, _, err := r.ReadRune() - if err != nil { - if err == io.EOF { - break - } - return nil, err - } - glyphs = append(glyphs, glyph{Rune: c}) - } - return glyphs, nil -} - -func (f *opentype) HasGlyph(buf *sfnt.Buffer, r rune) bool { - g, err := f.Font.GlyphIndex(buf, r) - return g != 0 && err == nil -} - -func (f *opentype) GlyphAdvance(buf *sfnt.Buffer, ppem fixed.Int26_6, r rune) (advance fixed.Int26_6, ok bool) { - g, err := f.Font.GlyphIndex(buf, r) - if err != nil { - return 0, false - } - adv, err := f.Font.GlyphAdvance(buf, g, ppem, f.Hinting) - return adv, err == nil -} - -func (f *opentype) Kern(buf *sfnt.Buffer, ppem fixed.Int26_6, r0, r1 rune) fixed.Int26_6 { - g0, err := f.Font.GlyphIndex(buf, r0) - if err != nil { - return 0 - } - g1, err := f.Font.GlyphIndex(buf, r1) - if err != nil { - return 0 - } - adv, err := f.Font.Kern(buf, g0, g1, ppem, f.Hinting) - if err != nil { - return 0 - } - return adv -} - -func (f *opentype) Metrics(buf *sfnt.Buffer, ppem fixed.Int26_6) font.Metrics { - m, _ := f.Font.Metrics(buf, ppem, f.Hinting) - return m -} - -func (f *opentype) Bounds(buf *sfnt.Buffer, ppem fixed.Int26_6) fixed.Rectangle26_6 { - r, _ := f.Font.Bounds(buf, ppem, f.Hinting) - return r -} - -func (f *opentype) LoadGlyph(buf *sfnt.Buffer, ppem fixed.Int26_6, r rune) ([]sfnt.Segment, bool) { - g, err := f.Font.GlyphIndex(buf, r) - if err != nil { - return nil, false - } - segs, err := f.Font.LoadGlyph(buf, g, ppem, nil) - if err != nil { - return nil, false - } - return segs, true -} diff --git a/font/opentype/opentype_test.go b/font/opentype/opentype_test.go index c47d24c2..682c7670 100644 --- a/font/opentype/opentype_test.go +++ b/font/opentype/opentype_test.go @@ -10,9 +10,7 @@ import ( "strings" "testing" - "golang.org/x/image/font" "golang.org/x/image/font/gofont/goregular" - "golang.org/x/image/font/sfnt" "golang.org/x/image/math/fixed" "gioui.org/internal/ops" @@ -43,9 +41,13 @@ func TestEmptyString(t *testing.T) { t.Fatalf("Layout returned no lines for empty string; expected 1") } l := lines[0] - exp, err := face.font.Bounds(new(sfnt.Buffer), ppem, font.HintingFull) - if err != nil { - t.Fatal(err) + exp := fixed.Rectangle26_6{ + Min: fixed.Point26_6{ + Y: fixed.Int26_6(-12094), + }, + Max: fixed.Point26_6{ + Y: fixed.Int26_6(2700), + }, } if got := l.Bounds; got != exp { t.Errorf("got bounds %+v for empty string; expected %+v", got, exp) diff --git a/widget/editor_test.go b/widget/editor_test.go index d33c1340..018c33fb 100644 --- a/widget/editor_test.go +++ b/widget/editor_test.go @@ -174,7 +174,7 @@ var arabic = system.Locale{ } var arabicCollection = func() []text.FontFace { - parsed, _ := opentype.ParseHarfbuzz(nsareg.TTF) + parsed, _ := opentype.Parse(nsareg.TTF) return []text.FontFace{{Font: text.Font{}, Face: parsed}} }() @@ -250,7 +250,7 @@ func TestEditorLigature(t *testing.T) { Constraints: layout.Exact(image.Pt(100, 100)), Locale: english, } - face, err := opentype.ParseHarfbuzz(robotoregular.TTF) + face, err := opentype.Parse(robotoregular.TTF) if err != nil { t.Skipf("failed parsing test font: %v", err) }