~sbinet/star-tex

86bb800d2d9d126983eb34f158ce523c4cf7e5f4 — Sebastien Binet 4 months ago bb815b9
font/tfm: implement Box, GlyphBounds, GlyphAdvance, Kern and Metrics

Signed-off-by: Sebastien Binet <s@sbinet.org>
5 files changed, 360 insertions(+), 77 deletions(-)

M font/tfm/encoder.go
D font/tfm/face.go
M font/tfm/font.go
M font/tfm/font_test.go
M font/tfm/tfm.go
M font/tfm/encoder.go => font/tfm/encoder.go +6 -6
@@ 291,7 291,7 @@ func (te *textEncoder) encodeLigTable(fnt *Font) {
	if lk := fnt.body.ligKern[0]; lk.raw[0] == 255 {
		te.boundarychar = int16(lk.raw[1])
		te.line("BOUNDARYCHAR" + te.char(byte(te.boundarychar)))
		te.act[0] = 1
		te.act[0] = actPassthrough
	}

	te.buildLabels(fnt)


@@ 316,7 316,7 @@ func (te *textEncoder) encodeLigTable(fnt *Font) {
			// FIXME(sbinet): check unconditional stop command.
		case lk.op() == krnCmd:
			line := "KRN" + te.char(byte(lk.nextChar()))
			ii := (int(lk.raw[2])-128)*256 + int(lk.raw[3])
			ii := lk.nextIndex()
			vv := fnt.body.kern[ii]
			line += " " + te.fword(vv)
			te.line(line)


@@ 378,8 378,8 @@ func (te *textEncoder) buildLabels(fnt *Font) {
			if lk.skipByte() {
				rem = int(lk.raw[2])*256 + int(lk.raw[3])
				if rem < len(fnt.body.ligKern) {
					if te.act[int(ci.raw[3])] == 0 {
						te.act[int(ci.raw[3])] = 1
					if te.act[int(ci.raw[3])] == actUnreachable {
						te.act[int(ci.raw[3])] = actPassthrough
					}
				}
			}


@@ 391,7 391,7 @@ func (te *textEncoder) buildLabels(fnt *Font) {
			))
		}

		te.act[rem] = 2
		te.act[rem] = actAccessible
		te.labels = append(te.labels, label{
			cc: int16(i),
			rr: rem,


@@ 445,7 445,7 @@ func (te *textEncoder) encodeChars(fnt *Font) {
					// FIXME(sinet): test for unconditional stop cmd address.
				case lk.raw[2] >= 128:
					line := "KRN" + te.char(byte(lk.raw[1]))
					rr := (int(lk.raw[2])-128)*256 + int(lk.raw[3])
					rr := lk.nextIndex()
					if rr >= len(fnt.body.kern) {
						panic("Bad TFM file: Kern index too large.")
					}

D font/tfm/face.go => font/tfm/face.go +0 -59
@@ 1,59 0,0 @@
// Copyright ©2021 The star-tex 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 tfm

import (
	"star-tex.org/x/tex/font/fixed"
)

// Face is a TFM font face.
type Face struct {
	font  *Font
	scale fixed.Int12_20

	buf []byte
}

// FaceOptions describes the possible options given to NewFace when
// creating a new Face from a Font.
type FaceOptions struct {
	Size fixed.Int12_20 // Size is the font size in DVI points.
}

func defaultFaceOptions(font *Font) *FaceOptions {
	return &FaceOptions{
		Size: font.DesignSize(),
	}
}

// NewFace returns a new font.Face for the given Font.
//
// If opts is nil, sensible defaults will be used.
func NewFace(font *Font, opts *FaceOptions) Face {
	if opts == nil {
		opts = defaultFaceOptions(font)
	}
	return Face{
		font:  font,
		scale: opts.Size,
		buf:   make([]byte, 4),
	}
}

// Name returns the name of the font face.
func (face *Face) Name() string {
	return face.font.Name()
}

// GlyphAdvance returns the advance width of r's glyph.
//
// It returns !ok if the face does not contain a glyph for r.
func (face *Face) GlyphAdvance(r rune) (adv fixed.Int12_20, ok bool) {
	adv, ok = face.font.GlyphAdvance(r)
	if !ok {
		return 0, ok
	}
	return fixed.Int12_20((int64(adv) * int64(face.scale)) >> 20), true
}

M font/tfm/font.go => font/tfm/font.go +201 -9
@@ 7,7 7,12 @@ package tfm
import (
	"bytes"
	"fmt"
	"image"
	"io"
	"math"

	"golang.org/x/image/font"
	xfix "golang.org/x/image/math/fixed"

	"star-tex.org/x/tex/font/fixed"
	"star-tex.org/x/tex/internal/iobuf"


@@ 25,6 30,9 @@ const (
type Font struct {
	hdr  fileHeader
	body fileBody

	metSet  bool // metSet tells whether the font metrics have been computed.
	metrics font.Metrics
}

type fileHeader struct {


@@ 82,33 90,217 @@ func (fnt *Font) NumGlyphs() int {
	return int(fnt.hdr.ec) + 1 - int(fnt.hdr.bc)
}

// GlyphIndex returns the GlyphIndex for the given rune.
// index returns the glyphIndex for the given rune.
//
// GlyphIndex returns -1 if there is no such rune.
func (fnt *Font) GlyphIndex(x rune) GlyphIndex {
	i := int(x)
// index returns -1 if there is no such rune.
func (fnt *Font) index(r rune) glyphIndex {
	i := int(r)
	if !(int(fnt.hdr.bc) <= i && i <= int(fnt.hdr.ec)) {
		panic(fmt.Errorf("glyph out of range"))
	}
	i -= int(fnt.hdr.bc)
	return GlyphIndex(i)
	return glyphIndex(i)
}

func (fnt *Font) glyph(x GlyphIndex) glyphInfo {
func (fnt *Font) glyph(x glyphIndex) glyphInfo {
	return fnt.body.glyphs[x]
}

// Box returns the width, height and depth of r's glyph.
//
// It returns !ok if the face does not contain a glyph for r.
func (fnt *Font) Box(r rune) (w, h, d fixed.Int12_20, ok bool) {
	i := int(r)
	if !(int(fnt.hdr.bc) <= i && i <= int(fnt.hdr.ec)) {
		return 0, 0, 0, false
	}
	i -= int(fnt.hdr.bc)
	g := fnt.body.glyphs[i]
	w = fnt.body.width[g.wd()] // FIXME(sbinet): apply italic correction?
	h = fnt.body.height[g.ht()]
	d = fnt.body.depth[g.dp()]
	return w, h, d, true
}

// GlyphAdvance returns the advance width of r's glyph.
//
// It returns !ok if the face does not contain a glyph for r.
func (fnt *Font) GlyphAdvance(x rune) (fixed.Int12_20, bool) {
	i := int(x)
func (fnt *Font) GlyphAdvance(r rune) (xfix.Int26_6, bool) {
	i := int(r)
	if !(int(fnt.hdr.bc) <= i && i <= int(fnt.hdr.ec)) {
		return 0, false
	}
	i -= int(fnt.hdr.bc)
	var (
		g  = fnt.body.glyphs[i]
		ic = fnt.body.italic[g.ic()]
		w  = fnt.body.width[g.wd()] + ic
	)
	return w.ToInt26_6(), true
}

// GlyphBounds returns the bounding box of r's glyph, drawn at a dot equal
// to the origin, and that glyph's advance width.
//
// It returns !ok if the face does not contain a glyph for r.
//
// The glyph's ascent and descent equal -bounds.Min.Y and +bounds.Max.Y. A
// visual depiction of what these metrics are is at
// https://developer.apple.com/library/mac/documentation/TextFonts/Conceptual/CocoaTextArchitecture/Art/glyph_metrics_2x.png
func (fnt *Font) GlyphBounds(r rune) (bounds xfix.Rectangle26_6, advance xfix.Int26_6, ok bool) {
	i := fnt.index(r)
	if i < 0 {
		return
	}
	ok = true
	g := fnt.body.glyphs[i]
	return fnt.body.width[g.wd()], true
	var (
		ic = fnt.body.italic[g.ic()]
		w  = fnt.body.width[g.wd()] + ic
		h  = fnt.body.height[g.ht()]
		d  = fnt.body.depth[g.dp()]
	)

	bounds = xfix.Rectangle26_6{
		Min: xfix.Point26_6{
			X: 0, // bearing?
			Y: -h.ToInt26_6(),
		},
		Max: xfix.Point26_6{
			X: w.ToInt26_6(),
			Y: d.ToInt26_6(),
		},
	}
	advance = w.ToInt26_6()

	return
}

// Kern returns the horizontal adjustment for the kerning pair (r0, r1). A
// positive kern means to move the glyphs further apart.
func (fnt *Font) Kern(r0, r1 rune) xfix.Int26_6 {
	i0 := fnt.index(r0)
	if i0 < 0 {
		return 0
	}
	i1 := fnt.index(r1)
	if i1 < 0 {
		return 0
	}

	g0 := fnt.glyph(i0)
	c1 := int(r1)
	if g0.raw[2]&3 == 1 {
		ii := int(g0.raw[3])
		rr := fnt.body.ligKern[ii]
		if rr.raw[0] > 128 {
			ii = int(rr.raw[2])*256 + int(rr.raw[3])
		}
		for {
			lk := fnt.body.ligKern[ii]
			switch {
			case lk.raw[2] >= 128:
				if int(lk.raw[1]) == c1 {
					rr := lk.nextIndex()
					kern := fnt.body.kern[rr]
					return fixed.Int12_20(kern).ToInt26_6()
				}
			default:
				// ok.
			}

			switch {
			case lk.raw[0] >= 128:
				ii = len(fnt.body.ligKern)
			default:
				ii += int(lk.raw[0]) + 1
			}

			if ii >= len(fnt.body.ligKern) {
				return 0
			}
		}
	}

	// FIXME(sbinet): implement it.
	return 0
}

// Metrics returns the metrics for this Face.
func (fnt *Font) Metrics() font.Metrics {
	if !fnt.metSet {
		fnt.metSet = true
		fnt.computeMetrics()
	}
	return fnt.metrics
}

func (fnt *Font) computeMetrics() {
	slant := fnt.body.param[0].Float64()
	slope := slopeFrom(slant)

	var (
		caph fixed.Int12_20
		asc  fixed.Int12_20
		desc fixed.Int12_20
	)
	for _, r := range "ABCDEFGHIJKLMNOPQRSTUVWXYZ" {
		idx := fnt.index(r)
		if idx < 0 {
			continue
		}
		g := fnt.glyph(idx)
		h := fnt.body.height[g.ht()]
		if h >= caph {
			caph = h
		}
	}

	// FIXME(sbinet): ascender and descender do not seem to be properly inferred
	// when using these heuristics.
	//	for _, r := range "abcdefghijklmnopqrstuvwxyz" {
	//		idx := fnt.index(r)
	//		if idx < 0 {
	//			continue
	//		}
	//		g := fnt.glyph(idx)
	//		h := fnt.body.height[g.ht()]
	//		d := fnt.body.depth[g.dp()]
	//		if h > asc {
	//			asc = h
	//		}
	//		if d > desc {
	//			desc = d
	//		}
	//	}

	fnt.metrics = font.Metrics{
		Ascent:     asc.ToInt26_6(),
		Descent:    desc.ToInt26_6(),
		XHeight:    fnt.body.param[4].ToInt26_6(),
		CapHeight:  caph.ToInt26_6(),
		CaretSlope: slope,
	}
}

func slopeFrom(slant float64) image.Point {
	if slant == 0 {
		return image.Pt(0, 1)
	}
	const epsilon = 1e-6
	var (
		v = math.Abs(slant)
		f = 1.0
	)
	for i := 0; i < 10; i++ {
		f = math.Pow10(i)
		r := math.Trunc(v * f)
		if math.Abs(r-v*f) < epsilon {
			break
		}
	}

	return image.Pt(int(f*slant), int(f))
}

func (fnt *Font) readHeader(r *iobuf.Reader) error {

M font/tfm/font_test.go => font/tfm/font_test.go +147 -1
@@ 5,12 5,16 @@
package tfm

import (
	"image"
	"os"
	"reflect"
	"strings"
	"testing"

	"golang.org/x/image/font"
	xfix "golang.org/x/image/math/fixed"
	"star-tex.org/x/tex/font/fixed"
	"star-tex.org/x/tex/kpath"
)

func TestFont(t *testing.T) {


@@ 278,7 282,7 @@ func TestParse(t *testing.T) {
				t.Fatalf("invalid TFM depth: got=%v, want=%v", got, want)
			}

			x := fnt.GlyphIndex('a')
			x := fnt.index('a')
			if x < 0 {
				t.Fatalf("could not find glyph")
			}


@@ 290,3 294,145 @@ func TestParse(t *testing.T) {
		})
	}
}

func TestCaretSlope(t *testing.T) {
	for _, tc := range []struct {
		slant float64
		caret image.Point
	}{
		{0, image.Pt(0, 1)},
		{+0.25, image.Pt(+25, 100)},
		{-0.25, image.Pt(-25, 100)},
		{+0.165549, image.Pt(+165549, 1e6)},
		{-0.165549, image.Pt(-165549, 1e6)},
	} {
		got := slopeFrom(tc.slant)
		if got != tc.caret {
			t.Errorf("invalid caret-slope from %v: got=%+v, want=%+v", tc.slant, got, tc.caret)
		}
	}
}

func TestMetrics(t *testing.T) {
	ktx := kpath.New()
	for _, tc := range []struct {
		name string
		want font.Metrics
	}{
		{
			name: "cmr10.tfm",
			want: font.Metrics{
				Height:     0, // FIXME
				Ascent:     0, // FIXME
				Descent:    0, // FIXME
				XHeight:    27,
				CapHeight:  43,
				CaretSlope: image.Point{X: 0, Y: 1},
			},
		},
		{
			name: "cmmi10.tfm",
			want: font.Metrics{
				Height:     0, // FIXME
				Ascent:     0, // FIXME
				Descent:    0, // FIXME
				XHeight:    27,
				CapHeight:  43,
				CaretSlope: image.Point{X: 25, Y: 100},
			},
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			fname, err := ktx.Find(tc.name)
			if err != nil {
				t.Fatalf("could not find TFM file: %+v", err)
			}

			f, err := ktx.Open(fname)
			if err != nil {
				t.Fatalf("could not open TFM file: %+v", err)
			}
			defer f.Close()

			fnt, err := Parse(f)
			if err != nil {
				t.Fatalf("could not parse TFM file: %+v", err)
			}

			got := fnt.Metrics()
			if got != tc.want {
				t.Fatalf("invalid metrics:\ngot: %+v\nwant:%+v", got, tc.want)
			}
		})
	}
}

func TestKern(t *testing.T) {
	type kern struct {
		r0, r1 rune
		v      xfix.Int26_6
	}
	ktx := kpath.New()
	for _, tc := range []struct {
		name string
		want []kern
	}{
		{
			name: "cmr10.tfm",
			want: []kern{
				{'A', 'a', 0},
				{'A', 't', -1},
				{'A', 'C', -1},
				{'A', 'O', -1},
				{'A', 'G', -1},
				{'A', 'U', -1},
				{'A', 'Q', -1},
				{'A', 'T', -5},
				{'A', 'Y', -5},
				{'A', 'V', -7},
				{'A', 'W', -7},
			},
		},
		{
			name: "cmsl10.tfm",
			want: []kern{
				{'A', 'a', 0},
				{'A', 't', -1},
				{'A', 'C', -1},
				{'A', 'O', -1},
				{'A', 'G', -1},
				{'A', 'U', -1},
				{'A', 'Q', -1},
				{'A', 'T', -5},
				{'A', 'Y', -5},
				{'A', 'V', -7},
				{'A', 'W', -7},
			},
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			fname, err := ktx.Find(tc.name)
			if err != nil {
				t.Fatalf("could not find TFM file: %+v", err)
			}

			f, err := ktx.Open(fname)
			if err != nil {
				t.Fatalf("could not open TFM file: %+v", err)
			}
			defer f.Close()

			fnt, err := Parse(f)
			if err != nil {
				t.Fatalf("could not parse TFM file: %+v", err)
			}

			for _, krn := range tc.want {
				got := fnt.Kern(krn.r0, krn.r1)
				if got != krn.v {
					t.Fatalf("invalid kern(%q,%q):\ngot: %d\nwant:%d", krn.r0, krn.r1, got, krn.v)
				}
			}
		})
	}
}

M font/tfm/tfm.go => font/tfm/tfm.go +6 -2
@@ 36,8 36,8 @@ func (ck glyphKind) String() string {
	}
}

// GlyphIndex is a glyph index in a Font.
type GlyphIndex int
// glyphIndex is a glyph index in a Font.
type glyphIndex int

// glyphInfo provides informations about a glyph.
type glyphInfo struct {


@@ 95,6 95,10 @@ func (lk ligKernCmd) op() ligKernOp {
	return krnCmd
}

func (lk ligKernCmd) nextIndex() int {
	return (int(lk.raw[2])-128)*256 + int(lk.raw[3])
}

type ligKernOp uint8

const (