~sbinet/star-tex

01f50749024c419b3b1800893d74a02994b9ecbb — Sebastien Binet 9 months ago 45a5485
{cmd/pk2bm,font/pkf}: first import

Fixes #8.

Signed-off-by: Sebastien Binet <s@sbinet.org>
A cmd/pk2bm/main.go => cmd/pk2bm/main.go +60 -0
@@ 0,0 1,60 @@
// 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 main // import "star-tex.org/x/tex/cmd/pk2bm"

import (
	"flag"
	"fmt"
	"io"
	"log"
	"os"

	"star-tex.org/x/tex/font/pkf"
)

func main() {
	log.SetPrefix("pk2bm: ")
	log.SetFlags(0)

	var (
		cflag  = flag.String("c", "", "character to display")
		hflag  = flag.Int("H", 0, "height of bitmap")
		wflag  = flag.Int("W", 0, "width of bitmap")
		bitmap = flag.Bool("b", false, "generate a bitmap")
		hexmap = flag.Bool("h", false, "generate a hexmap")
	)

	flag.Parse()

	if *hexmap && *bitmap {
		log.Fatalf("you need to chose either -h or -b")
	}

	f, err := os.Open(flag.Arg(0))
	if err != nil {
		log.Fatalf("could not open PK file: %+v", err)
	}
	defer f.Close()

	err = process(os.Stdout, f, rune((*cflag)[0]), *hflag, *wflag, *bitmap, *hexmap)
	if err != nil {
		log.Fatalf("could not process PK file: %+v", err)
	}
}

func process(o io.Writer, r io.Reader, c rune, h, w int, bitmap, hexmap bool) error {
	f, err := pkf.Parse(r)
	if err != nil {
		return fmt.Errorf("could not parse PK font: %w", err)
	}
	switch {
	case bitmap:
		return f.Bitmap(o, c, h, w)
	case hexmap:
		return f.Hexmap(o, c, h, w)
	default:
		return f.Rawmap(o, c, h, w)
	}
}

A cmd/pk2bm/main_test.go => cmd/pk2bm/main_test.go +169 -0
@@ 0,0 1,169 @@
// 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 main // import "star-tex.org/x/tex/cmd/pk2bm"

import (
	"bytes"
	"os"
	"os/exec"
	"path/filepath"
	"strconv"
	"testing"

	"star-tex.org/x/tex/kpath"
)

func TestPK2BM(t *testing.T) {
	ktx := kpath.New()

	fname, err := ktx.Find("cmr10.pk")
	if err != nil {
		t.Fatalf("could not find cmr10: %+v", err)
	}

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

	const (
		bitmap = true
		hexmap = false
		h      = 0
		w      = 0
		c      = 'a'
	)

	got := new(bytes.Buffer)
	err = process(got, pkf, rune(c), h, w, bitmap, hexmap)
	if err != nil {
		t.Fatalf("could not process char=%c: %+v", c, err)
	}

	if got, want := got.String(), wantA; got != want {
		t.Fatalf("invalid pkf2bm output:\ngot:\n%s\nwant:\n%s", got, want)
	}
}

func TestTeXPK2BM(t *testing.T) {
	if cmd, err := exec.LookPath("/usr/bin/pk2bm"); err != nil || cmd == "" {
		t.Skipf("skipping comparison against TeX pk2bm")
	}

	const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789`!@#$%^&*()_+-=[]{};'\\:\"|/.,<>?"
	const cmdir = "../../internal/tds/fonts/pk/ljfour/public/cm/dpi600"

	files, err := os.ReadDir(cmdir)
	if err != nil {
		t.Fatalf("could not open cm PK dir: %+v", err)
	}

	for _, file := range files {
		cmr10 := filepath.Join(cmdir, file.Name())
		pkf, err := os.ReadFile(cmr10)
		if err != nil {
			t.Fatalf("could not open cmr10 PK file: %+v", err)
		}
		t.Run(cmr10, func(t *testing.T) {

			for _, tc := range []byte(chars) {
				t.Run(string(tc), func(t *testing.T) {
					const (
						bitmap = true
						hexmap = false
						h      = 0
						w      = 0
					)
					got := new(bytes.Buffer)
					err := process(got, bytes.NewReader(pkf), rune(tc), h, w, bitmap, hexmap)
					if err != nil {
						t.Fatalf("could not process char=%c: %+v", tc, err)
					}

					args := []string{
						"-c", string(tc),
						"-H", strconv.Itoa(h),
						"-W", strconv.Itoa(w),
						cmr10,
					}

					switch {
					case bitmap:
						args = append([]string{"-b"}, args...)
					case hexmap:
						args = append([]string{"-h"}, args...)
					}

					want := new(bytes.Buffer)
					cmd := exec.Command(
						"/usr/bin/pk2bm",
						args...,
					)
					cmd.Stdout = want
					cmd.Stderr = want

					err = cmd.Run()
					if err != nil {
						t.Logf("===\n%s\n===\n", want.String())
						t.Fatalf("could not run TeX pk2bm: %+v", err)
					}

					if got, want := got.String(), want.String(); got != want {
						t.Fatalf("invalid pkf2bm output:\ngot:\n%s\nwant:\n%s", got, want)
					}
				})
			}
		})
	}
}

const wantA = `
character : 97 (a)
   height : 39
    width : 38
     xoff : -3
     yoff : 37

  ...........********...................
  ........**************................
  ......*****.......******..............
  .....***............*****.............
  ....*****............******...........
  ...*******............******..........
  ...********...........******..........
  ...********............******.........
  ...********............******.........
  ...********.............******........
  ....******..............******........
  .....****...............******........
  ........................******........
  ........................******........
  ........................******........
  ........................******........
  .................*************........
  .............*****************........
  ..........*********.....******........
  ........*******.........******........
  ......*******...........******........
  ....********............******........
  ...*******..............******........
  ..********..............******........
  .********...............******........
  .*******................******........
  .*******................******......**
  *******.................******......**
  *******.................******......**
  *******.................******......**
  *******................*******......**
  *******................*******......**
  ********..............********......**
  .*******.............***.*****......**
  .********............**...*****....**.
  ..********.........****...*****....**.
  ....*******......****......*********..
  ......**************........*******...
  .........********............*****....
`

A font/pkf/bitmap.go => font/pkf/bitmap.go +145 -0
@@ 0,0 1,145 @@
// 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 pkf

import (
	"fmt"
	"io"
)

// Bitmap displays the rune c as an ASCII bitmap to the provided writer.
func (fnt *Font) Bitmap(o io.Writer, c rune, h, w int) error {
	const raw = false
	return fnt.displayGlyph(o, c, h, w, raw, func(w io.Writer, u uint8, n int) {
		bit := uint8(1 << 7)
		for ; n > 0; n-- {
			switch {
			case u&bit != 0:
				fmt.Fprintf(w, "*")
			default:
				fmt.Fprintf(w, ".")
			}
			bit >>= 1
		}
	})
}

// Hexmap displays the rune c as an ASCII hexmap to the provided writer.
func (fnt *Font) Hexmap(o io.Writer, c rune, h, w int) error {
	const raw = false
	return fnt.displayGlyph(o, c, h, w, raw, func(w io.Writer, v uint8, n int) {
		fmt.Fprintf(w, "%02x", v)
	})
}

// Rawmap displays the rune c as an ASCII rawmap to the provided writer.
func (fnt *Font) Rawmap(o io.Writer, c rune, h, w int) error {
	const raw = true
	return fnt.displayGlyph(o, c, h, w, raw, func(w io.Writer, v uint8, n int) {
		fmt.Fprintf(w, "0x%02x, ", lsbf(v))
	})
}

func (fnt *Font) displayGlyph(o io.Writer, c rune, h, w int, raw bool, fun func(w io.Writer, u uint8, n int)) error {
	var g *Glyph
	for i := range fnt.glyphs {
		if fnt.glyphs[i].code == uint32(c) {
			g = &fnt.glyphs[i]
			g.unpack()
			break
		}
	}
	if g == nil {
		return fmt.Errorf("could not find glyph 0x%x", c)
	}

	var (
		H, dh int
		W, dw int
	)

	H = int(g.height)
	if h == 0 {
		h = H
	}
	dh = (h - H) / 2

	W = int(g.width)
	if w == 0 {
		w = W
	}
	dw = (w - W) / 2

	fmt.Fprintf(o, "\n")
	switch {
	case raw:
		fmt.Fprintf(o, "#define %s_%c_width \t %d\n", "fname", g.code, w)
		fmt.Fprintf(o, "#define %s_%c_height \t %d\n", "fname", g.code, h)
		fmt.Fprintf(o, "#define %s_%c_xoff \t %d\n", "fname", g.code, dw)
		fmt.Fprintf(o, "#define %s_%c_yoff \t %d\n", "fname", g.code, dh)
		fmt.Fprintf(o, "static char %s_%c_bits[] = {", "fname", g.code)
	default:
		fmt.Fprintf(o, "character : %d (%c)\n", g.code, g.code)
		fmt.Fprintf(o, "   height : %d\n", g.height)
		fmt.Fprintf(o, "    width : %d\n", g.width)
		fmt.Fprintf(o, "     xoff : %d\n", g.xoff)
		fmt.Fprintf(o, "     yoff : %d\n", g.yoff)
	}

	for row := 0; row < h-H-dh; row++ {
		fmt.Fprintf(o, "\n  ")
		for col := 0; col < w; col += 8 {
			n := clip(w-col, 8)
			fun(o, 0, n)
		}
	}

	var i int
	for row := 0; row < int(g.height); row++ {
		fmt.Fprintf(o, "\n  ")
		for col := 0; col < int(g.width); col += 8 {
			v := g.mask[i]
			n := clip(int(g.width)-col, 8)
			fun(o, v, n)
			i++
		}
	}

	for row := h - dh; row < h; row++ {
		fmt.Fprintf(o, "\n  ")
		for col := 0; col < w; col += 8 {
			n := clip(w-col, 8)
			fun(o, 0, n)
		}
	}

	switch {
	case raw:
		fmt.Fprintf(o, "};\n")
	default:
		fmt.Fprintf(o, "\n")
	}

	return nil
}

func clip(v, max int) int {
	if v >= max {
		return max
	}
	return v
}

func lsbf(u uint8) uint8 {
	var (
		bit, o uint8
	)
	for i := 0; i < 8; i++ {
		bit = u & 0o1
		o = (o << 1) | bit
		u = u >> 1
	}
	return o
}

A font/pkf/cmd.go => font/pkf/cmd.go +125 -0
@@ 0,0 1,125 @@
// 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 pkf

import (
	"star-tex.org/x/tex/internal/iobuf"
)

// Cmd is a PK command.
type Cmd interface {
	opcode() opCode
	Name() string

	read(r *iobuf.Reader)
}

type CmdXXX1 struct {
	Value []byte
}

func (CmdXXX1) opcode() opCode { return opXXX1 }
func (CmdXXX1) Name() string   { return "pk_xxx1" }
func (cmd *CmdXXX1) read(r *iobuf.Reader) {
	_ = r.ReadU8()
	n := int(r.ReadU8())
	cmd.Value = r.ReadBuf(n)
}

type CmdXXX2 struct {
	Value []byte
}

func (CmdXXX2) opcode() opCode { return opXXX2 }
func (CmdXXX2) Name() string   { return "pk_xxx2" }
func (cmd *CmdXXX2) read(r *iobuf.Reader) {
	_ = r.ReadU8()
	n := int(r.ReadU16())
	cmd.Value = r.ReadBuf(n)
}

type CmdXXX3 struct {
	Value []byte
}

func (CmdXXX3) opcode() opCode { return opXXX3 }
func (CmdXXX3) Name() string   { return "pk_xxx3" }
func (cmd *CmdXXX3) read(r *iobuf.Reader) {
	_ = r.ReadU8()
	n := int(r.ReadU24())
	cmd.Value = r.ReadBuf(n)
}

type CmdXXX4 struct {
	Value []byte
}

func (CmdXXX4) opcode() opCode { return opXXX4 }
func (CmdXXX4) Name() string   { return "pk_xxx4" }
func (cmd *CmdXXX4) read(r *iobuf.Reader) {
	_ = r.ReadU8()
	n := int(r.ReadU32())
	cmd.Value = r.ReadBuf(n)
}

type CmdYYY struct {
	Value uint32
}

func (CmdYYY) opcode() opCode { return opYYY }
func (CmdYYY) Name() string   { return "pk_yyy" }
func (cmd *CmdYYY) read(r *iobuf.Reader) {
	_ = r.ReadU8()
	cmd.Value = r.ReadU32()
}

type CmdPost struct{}

func (CmdPost) opcode() opCode { return opPost }
func (CmdPost) Name() string   { return "pk_post" }
func (CmdPost) read(r *iobuf.Reader) {
	_ = r.ReadU8()
}

type CmdNOP struct{}

func (CmdNOP) opcode() opCode { return opNOP }
func (CmdNOP) Name() string   { return "pk_nop" }
func (CmdNOP) read(r *iobuf.Reader) {
	_ = r.ReadU8()
}

type CmdPre struct {
	Version  uint8
	Msg      string
	Design   uint32
	Checksum uint32
	Hppp     uint32
	Vppp     uint32
}

func (CmdPre) opcode() opCode { return opPre }
func (CmdPre) Name() string   { return "pk_pre" }
func (cmd *CmdPre) read(r *iobuf.Reader) {
	_ = r.ReadU8()
	cmd.Version = r.ReadU8()
	n := int(r.ReadU8())
	cmd.Msg = string(r.ReadBuf(n))
	cmd.Design = r.ReadU32()
	cmd.Checksum = r.ReadU32()
	cmd.Hppp = r.ReadU32()
	cmd.Vppp = r.ReadU32()
}

var (
	_ Cmd = (*CmdXXX1)(nil)
	_ Cmd = (*CmdXXX2)(nil)
	_ Cmd = (*CmdXXX3)(nil)
	_ Cmd = (*CmdXXX4)(nil)
	_ Cmd = (*CmdYYY)(nil)
	_ Cmd = (*CmdPost)(nil)
	_ Cmd = (*CmdNOP)(nil)
	_ Cmd = (*CmdPre)(nil)
)

A font/pkf/face.go => font/pkf/face.go +242 -0
@@ 0,0 1,242 @@
// 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 pkf

import (
	"image"

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

	tfix "star-tex.org/x/tex/font/fixed"
	"star-tex.org/x/tex/font/tfm"
)

// Face implements the font.Face interface for PK fonts.
type Face struct {
	font  *Font
	tfm   *tfm.Font
	scale fixed.Int26_6

	glyphs map[rune]int
}

// FaceOptions describes the possible options given to NewFace when
// creating a new Face from a Font.
type FaceOptions struct {
	Size float64 // Size is the font size in DVI points.
	DPI  float64 // DPI is the dots per inch resolution
}

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

func NewFace(font *Font, metrics *tfm.Font, opts *FaceOptions) *Face {
	if opts == nil {
		opts = defaultFaceOptions(metrics)
	}
	return &Face{
		font:   font,
		tfm:    metrics,
		scale:  fixed.Int26_6(0.5 + (opts.Size * opts.DPI * 64 / 72)),
		glyphs: make(map[rune]int, len(font.glyphs)/4),
	}
}

// xscale returns x divided by unitsPerEm, rounded to the nearest fixed.Int26_6
// value (1/64th of a pixel).
func xscale(x fixed.Int26_6, unitsPerEm Units) fixed.Int26_6 {
	u := fixed.Int26_6(unitsPerEm)
	v := u / 2
	switch {
	case x >= 0:
		x += v
	default:
		x -= v
	}
	return x / u
}

// Close satisfies the font.Face interface.
func (*Face) Close() error {
	return nil
}

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

// Glyph returns the draw.DrawMask parameters (dr, mask, maskp) to draw r's
// glyph at the sub-pixel destination location dot, and that glyph's
// advance width.
//
// It returns !ok if the face does not contain a glyph for r.
//
// The contents of the mask image returned by one Glyph call may change
// after the next Glyph call. Callers that want to cache the mask must make
// a copy.
func (face *Face) Glyph(dot fixed.Point26_6, r rune) (dr image.Rectangle, mask image.Image, maskp image.Point, advance fixed.Int26_6, ok bool) {

	g, ok := face.glyph(r)
	if !ok {
		return
	}

	g.unpack()

	advance, ok = face.glyphAdvance(r, g)
	if !ok {
		return
	}

	dr = image.Rect(
		-int(g.xoff),
		-int(g.yoff),
		-int(g.xoff)+int(g.width),
		-int(g.yoff)+int(g.height),
	).Add(image.Pt(dot.X.Floor(), dot.Y.Floor()))

	msk := g.Mask()
	mask = &msk
	ok = true
	return
}

// 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 are equal to -bounds.Min.Y and
// +bounds.Max.Y. The glyph's left-side and right-side bearings are equal
// to bounds.Min.X and advance-bounds.Max.X. A visual depiction of what
// these metrics are is at
// https://developer.apple.com/library/archive/documentation/TextFonts/Conceptual/CocoaTextArchitecture/Art/glyphterms_2x.png
func (face *Face) GlyphBounds(r rune) (bounds fixed.Rectangle26_6, advance fixed.Int26_6, ok bool) {
	g, ok := face.glyph(r)
	if !ok {
		return
	}
	return face.glyphBounds(r, g)
}

func (face *Face) glyphBounds(r rune, g *Glyph) (bounds fixed.Rectangle26_6, advance fixed.Int26_6, ok bool) {
	bounds, _, ok = face.tfm.GlyphBounds(r)
	if !ok {
		return
	}
	advance, ok = face.glyphAdvance(r, g)
	if !ok {
		return
	}

	em := face.font.UnitsPerEm()
	rescale := func(v fixed.Int26_6) fixed.Int26_6 {
		v *= fixed.Int26_6(em)
		v /= 1 << 6
		return v
	}

	bounds.Min.X = rescale(bounds.Min.X)
	bounds.Min.Y = rescale(bounds.Min.Y)
	bounds.Max.X = rescale(bounds.Max.X)
	bounds.Max.Y = rescale(bounds.Max.Y)

	bounds.Min.X = xscale(bounds.Min.X*face.scale, em)
	bounds.Min.Y = xscale(bounds.Min.Y*face.scale, em)
	bounds.Max.X = xscale(bounds.Max.X*face.scale, em)
	bounds.Max.Y = xscale(bounds.Max.Y*face.scale, em)

	dx := tfix.Int12_20(g.dx).ToInt26_6()
	dy := tfix.Int12_20(g.dy).ToInt26_6()

	bounds.Min.X += dx
	bounds.Max.X -= dx
	bounds.Min.Y += dy // FIXME(sbinet): check sign of vertical displacement
	bounds.Max.Y -= dy // FIXME(sbinet): check sign of vertical displacement

	return bounds, advance, ok
}

// 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) (advance fixed.Int26_6, ok bool) {
	g, ok := face.glyph(r)
	if !ok {
		return
	}
	return face.glyphAdvance(r, g)
}

func (face *Face) glyphAdvance(r rune, g *Glyph) (advance fixed.Int26_6, ok bool) {
	advance, ok = face.tfm.GlyphAdvance(r)
	if !ok {
		return 0, false
	}

	em := face.font.UnitsPerEm()
	advance *= fixed.Int26_6(em) // FIXME(sbinet): by trial and error.
	advance /= 1 << 6            // figure out why we need this.

	advance = xscale(advance*face.scale, em)
	return advance, true
}

// Kern returns the horizontal adjustment for the kerning pair (r0, r1). A
// positive kern means to move the glyphs further apart.
func (face *Face) Kern(r0, r1 rune) fixed.Int26_6 {
	k := face.tfm.Kern(r0, r1)
	return xscale(k*face.scale, face.font.UnitsPerEm())
}

// Metrics returns the metrics for this Face.
func (face *Face) Metrics() font.Metrics {
	em := face.font.UnitsPerEm()

	met := face.tfm.Metrics()

	rescale := func(v fixed.Int26_6) fixed.Int26_6 {
		v *= fixed.Int26_6(em)
		v /= 1 << 6
		return v
	}

	met.Height = rescale(met.Height)
	met.Ascent = rescale(met.Ascent)
	met.Descent = rescale(met.Descent)
	met.XHeight = rescale(met.XHeight)
	met.CapHeight = rescale(met.CapHeight)

	met.Height = xscale(met.Height*face.scale, em)
	met.Ascent = xscale(met.Ascent*face.scale, em)
	met.Descent = xscale(met.Descent*face.scale, em)
	met.XHeight = xscale(met.XHeight*face.scale, em)
	met.CapHeight = xscale(met.CapHeight*face.scale, em)

	return met
}

func (face *Face) glyph(r rune) (*Glyph, bool) {
	if i, ok := face.glyphs[r]; ok {
		return &face.font.glyphs[i], true
	}
	i := face.font.index(r)
	if i < 0 {
		return nil, false
	}
	face.glyphs[r] = i
	return &face.font.glyphs[i], true

}

var (
	_ font.Face = (*Face)(nil)
)

A font/pkf/font.go => font/pkf/font.go +145 -0
@@ 0,0 1,145 @@
// 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 pkf

import (
	"fmt"
	"io"

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

// Units are an integral number of abstract, scalable "font units". The em
// square is typically 1000 or 2048 "font units". This would map to a certain
// number (e.g. 30 pixels) of physical pixels, depending on things like the
// display resolution (DPI) and font size (e.g. a 12 point font).
type Units int32

// Font is a Packed Font.
type Font struct {
	hdr    CmdPre
	glyphs []Glyph
}

// Parse parses a Packed Font file.
func Parse(r io.Reader) (*Font, error) {
	raw, err := io.ReadAll(r)
	if err != nil {
		return nil, err
	}
	rr := iobuf.NewReader(raw)

	if opCode(rr.PeekU8()) != opPre {
		return nil, fmt.Errorf("pkf: invalid PK header")
	}

	var fnt Font
	fnt.hdr.read(rr)

specials:
	for {
		op := opCode(rr.PeekU8())
		if op < opXXX1 || op == opPost {
			break specials
		}
		switch op {
		case opXXX1, opXXX2, opXXX3, opXXX4:
			op.cmd().read(rr)
		case opYYY:
			op.cmd().read(rr)
		case opNOP:
			op.cmd().read(rr)
		case 247, 248, 249, 250, 251, 252, 253, 254, 255:
			return nil, fmt.Errorf("pkf: unexpected PK flagbyte 0x%x (%d)", op, op)
		}
	}

loop:
	for {
		op := opCode(rr.PeekU8())
		switch op {
		case opPost:
			break loop
		case opNOP:
			op.cmd().read(rr)
		case opXXX1, opXXX2, opXXX3, opXXX4:
			op.cmd().read(rr)
		case opYYY:
			op.cmd().read(rr)
		default:
			switch {
			case op < opXXX1:
				glyph, err := readGlyph(rr)
				if err != nil {
					return nil, err
				}
				fnt.glyphs = append(fnt.glyphs, glyph)
			default:
				return nil, fmt.Errorf("pkf: invalid opcode 0x%x (%d)", op, op)
			}
		}
	}
	return &fnt, nil
}

// UnitsPerEm returns the number of units per em for that font.
func (fnt *Font) UnitsPerEm() Units {
	// FIXME(sbinet): extract or infer from TFM.body.param ?
	return 1000
}

// DesignSize returns the TFM/PK font's design size.
func (fnt *Font) DesignSize() fixed.Int12_20 {
	return fixed.Int12_20(fnt.hdr.Design)
}

// Checksum returns the PK font checksum of that font.
// Checksum should be equal to the TFM checksum.
func (fnt *Font) Checksum() uint32 {
	return fnt.hdr.Checksum
}

// NumGlyphs returns the number of glyphs in this font.
func (fnt *Font) NumGlyphs() int {
	return len(fnt.glyphs)
}

// GlyphAt returns the i-th glyph from the PK font.
func (fnt *Font) GlyphAt(i int) *Glyph {
	if i < 0 || len(fnt.glyphs) <= i {
		return nil
	}
	return &fnt.glyphs[i]
}

// Glyph returns the glyph corresponding to the provided rune r,
// or nil if it is not present in the PK font.
func (fnt *Font) Glyph(r rune) *Glyph {
	g, ok := fnt.glyph(r)
	if !ok {
		return nil
	}
	return g
}

func (fnt *Font) index(r rune) int {
	for i := range fnt.glyphs {
		if fnt.glyphs[i].code == uint32(r) {
			return i
		}
	}
	return -1
}

func (fnt *Font) glyph(r rune) (*Glyph, bool) {
	for i := range fnt.glyphs {
		g := &fnt.glyphs[i]
		if g.code == uint32(r) {
			return g, true
		}
	}
	return nil, false
}

A font/pkf/font_test.go => font/pkf/font_test.go +59 -0
@@ 0,0 1,59 @@
// 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 pkf

import (
	"testing"

	"star-tex.org/x/tex/font/fixed"
	"star-tex.org/x/tex/kpath"
)

func TestFont(t *testing.T) {
	ctx := kpath.New()
	for _, tc := range []struct {
		name      string
		numglyphs int
		design    fixed.Int12_20
		chksum    uint32
	}{
		{
			name:      "fonts/pk/ljfour/public/cm/dpi600/cmr10.pk",
			numglyphs: 128,
			design:    10485760,
			chksum:    1274110073,
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			f, err := ctx.Open(tc.name)
			if err != nil {
				t.Fatalf("could not open PK file: %+v", err)
			}
			defer f.Close()

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

			if got, want := fnt.DesignSize(), tc.design; got != want {
				t.Fatalf("invalid design size: got=%v, want=%v", got, want)
			}

			if got, want := fnt.Checksum(), tc.chksum; got != want {
				t.Fatalf("invalid checksum: got=%v, want=%v", got, want)
			}

			if got, want := fnt.NumGlyphs(), tc.numglyphs; got != want {
				t.Fatalf("invalid number of glyphs: got=%d, want=%d", got, want)
			}

			for i := 0; i < fnt.NumGlyphs(); i++ {
				g := fnt.GlyphAt(i)
				g.unpack()
			}
		})
	}
}

A font/pkf/glyph.go => font/pkf/glyph.go +420 -0
@@ 0,0 1,420 @@
// 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 pkf

import (
	"fmt"
	"image"

	"star-tex.org/x/tex/internal/iobuf"
)

// Glyph represents a glyph contained in a PK font file.
type Glyph struct {
	flag   uint8
	code   uint32 // character code
	wtfm   uint32 // TFM width
	dx     uint32 // horizontal escapement
	dy     uint32 // vertical escapement
	width  uint32 // width in pixels of the minimum bounding box
	height uint32 // height in pixels of the minimum bounding box
	xoff   int32  // horizontal offset from the upper left pixel
	yoff   int32  // vertical offset from the upper left pixel

	data []byte
	mask []byte
}

func (g *Glyph) unpack() {
	if g.mask != nil {
		return
	}

	gr := glyphReader{
		r: iobuf.NewReader(g.data),
		g: g,
	}
	g.mask = gr.unpack()
}

func (g *Glyph) Mask() image.Alpha {
	g.unpack()
	h := int(g.height)
	w := int(g.width)
	pix := make([]byte, 0, h*w)
	var i int
	for row := 0; row < h; row++ {
		for col := 0; col < w; col += 8 {
			v := g.mask[i]
			n := clip(w-col, 8)
			bit := uint8(1 << 7)
			for ; n > 0; n-- {
				switch {
				case v&bit != 0:
					pix = append(pix, 0xff)
				default:
					pix = append(pix, 0x00)
				}
				bit >>= 1
			}
			i++
		}
	}

	return image.Alpha{
		Stride: w,
		Pix:    pix,
		Rect:   image.Rect(0, 0, w, h),
	}
}

func (g *Glyph) Bounds() image.Rectangle {
	h := int(g.height)
	w := int(g.width)
	return image.Rect(0, 0, w, h)
}

func readGlyph(r *iobuf.Reader) (g Glyph, err error) {
	var (
		pos    = r.Pos()
		raster uint32
	)

	g.flag = r.ReadU8()
	switch g.flag & 7 {
	case 0, 1, 2, 3:
		// 'short' character description.
		// flag[1] pl[1] cc[1] tfm[3] dm[1] w[1] h[1] hoff[+1] voff[+1]
		raster = uint32(g.flag&7)*(2<<7) + uint32(r.ReadU8()) - 4
		g.code = uint32(r.ReadU8())
		g.wtfm = r.ReadU24()
		g.dx = uint32(r.ReadU8()) * 65536
		g.dy = 0
		g.width = uint32(r.ReadU8())
		g.height = uint32(r.ReadU8())
		g.xoff = int32(r.ReadI8())
		g.yoff = int32(r.ReadI8())
		raster -= 4
	case 4, 5, 6:
		// 'extended short' character description.
		// flag[1] pl[2] cc[1] tfm[3] dm[2] w[2] h[2] hoff[+2] voff[+2].
		raster = uint32(g.flag&3)*(2<<15) + uint32(r.ReadU16()) - 5
		g.code = uint32(r.ReadU8())
		g.wtfm = r.ReadU24()
		g.dx = uint32(r.ReadU16()) * 65536
		g.dy = 0
		g.width = uint32(r.ReadU16())
		g.height = uint32(r.ReadU16())
		g.xoff = int32(r.ReadI16())
		g.yoff = int32(r.ReadI16())
		raster -= 4 * 2
	case 7:
		// 'long' character description.
		// flag[1] pl[4] cc[4] tfm[4] dx[4] dy[4] w[4] h[4] hoff[4] voff[4]
		raster = r.ReadU32() - 12
		g.code = r.ReadU32()
		g.wtfm = r.ReadU32()
		g.dx = r.ReadU32()
		g.dy = r.ReadU32()
		g.width = r.ReadU32()
		g.height = r.ReadU32()
		g.xoff = int32(r.ReadU32())
		g.yoff = int32(r.ReadU32())
		raster -= 4 * 4
	}
	g.data = r.ReadBuf(int(raster))
	g.mask = nil

	if false {
		dynf := g.flag / 16

		fmt.Printf(
			"%d:  Flag byte = %d  Character = %d  Packet length = %d\n"+
				"  Dynamic packing variable = %d\n"+
				"  TFM width = %d  dx = %d%s\n"+
				"  Height = %d  Width = %d  X-offset = %d  Y-offset = %d\n",
			pos, g.flag, g.code, raster,
			dynf,
			g.wtfm, g.dx, func() string {
				switch g.dy {
				case 0:
					return " "
				default:
					return fmt.Sprintf("  dy = %d", g.dy)
				}
			}(),
			g.height, g.width, g.xoff, g.yoff,
		)
	}

	return g, err
}

type glyphReader struct {
	r *iobuf.Reader
	g *Glyph

	inputbyte uint16
	bitweight uint16
	dynf      uint32
	repeat    uint32
	remainder int32
	read      func() uint32
}

func (gr *glyphReader) init() {
	gr.r.SetPos(0)
	gr.repeat = 0

	gr.inputbyte = 0
	gr.bitweight = 0
	gr.dynf = uint32(gr.g.flag / 16)
	gr.read = gr.pknum
}

var gpower = [17]uint16{
	0, 1, 3, 7, 15, 31, 63, 127,
	255, 511, 1023, 2047, 4095, 8191, 16383, 32767, 65535,
}

func (gr *glyphReader) unpack() []byte {
	var (
		wordwidth  = int16((gr.g.width + 15) / 16)
		word       uint16
		wordweight uint16
		rowsleft   int16
		turnon     = gr.g.flag&8 != 0
		hbit       int16
		count      uint16

		mask []uint8
	)

	gr.init()

	sz := 2 * gr.g.height * uint32(wordwidth)
	if sz <= 0 {
		sz = 2
	}

	var (
		idx    int
		sli    = make([]uint16, sz/2+1) // divide by 2: sz is in bytes
		raster = sli[1:]
	)

	switch gr.dynf {
	case 14:
		gr.bitweight = 0
		for i := 0; i < int(gr.g.height); i++ {
			word = 0
			wordweight = 32768
			for j := 0; j < int(gr.g.width); j++ {
				if gr.getbit() {
					word += wordweight
				}
				wordweight >>= 1
				if wordweight == 0 {
					raster[idx] = word
					idx++
					word = 0
					wordweight = 32768
				}
			}
			if wordweight != 32768 {
				raster[idx] = word
				idx++
			}
		}
	default:
		rowsleft = int16(gr.g.height)
		hbit = int16(gr.g.width)
		wordweight = 16
		word = 0
		for rowsleft > 0 {
			count = uint16(gr.read())
			for count != 0 {
				switch {
				case count < wordweight && count < uint16(hbit):
					if turnon {
						word += gpower[wordweight] - gpower[wordweight-count]
					}
					hbit -= int16(count)
					wordweight -= count
					count = 0

				case count >= uint16(hbit) && uint16(hbit) <= wordweight:
					if turnon {
						word += gpower[wordweight] - gpower[wordweight-uint16(hbit)]
					}
					raster[idx] = word
					idx++
					for i := 0; i < int(gr.repeat); i++ {
						for j := 0; j < int(wordwidth); j++ {
							raster[idx] = raster[idx-int(wordwidth)]
							idx++
						}
					}
					rowsleft -= int16(gr.repeat) + 1
					gr.repeat = 0
					word = 0
					wordweight = 16
					count -= uint16(hbit)
					hbit = int16(gr.g.width)
				default:
					if turnon {
						word += gpower[wordweight]
					}
					raster[idx] = word
					idx++
					word = 0
					count -= wordweight
					hbit -= int16(wordweight)
					wordweight = 16
				}
			}
			turnon = !turnon
		}
		if rowsleft != 0 || hbit != int16(gr.g.width) {
			panic(fmt.Errorf("error while unpacking: more bits than required: rowsleft=%d hbit=%d width=%d",
				rowsleft, hbit, gr.g.width,
			))
		}
	}

	{
		// build raster data
		var (
			widx = 0
			word = sli
		)
		for row := 0; row < int(gr.g.height); row++ {
			var (
				bitsleft uint8
				nextword uint16
				nextbyte uint8
			)
			for col := 0; col < int(gr.g.width); col += 8 {
				switch {
				case bitsleft >= 8:
					nextbyte = uint8(nextword >> (bitsleft - 8) & 0xff)
					bitsleft -= 8
					mask = append(mask, nextbyte)
				default:
					nextbyte = uint8(nextword << (8 - bitsleft) & 0xff)
					widx++
					nextword = word[widx]
					nextbyte = nextbyte | uint8(nextword>>(16-(8-bitsleft))&0xff)
					bitsleft = 16 - (8 - bitsleft)
					mask = append(mask, nextbyte)
				}
			}
		}
	}

	return mask
}

func (gr *glyphReader) pkbyte() uint16 {
	return uint16(gr.r.ReadU8())
}

func (gr *glyphReader) pknum() uint32 {
	var (
		i, j uint16
		dynf = uint16(gr.dynf)
	)
	i = uint16(gr.nyb())
	switch {
	case i == 0:
		for {
			j = uint16(gr.nyb())
			i++
			if j != 0 {
				break
			}
		}
		switch {
		case i > 3:
			return gr.huge(i, j)
		default:
			for i > 0 {
				j = j*16 + uint16(gr.nyb())
				i--
			}
			return uint32(j - 15 + (13-dynf)*16 + dynf)
		}
	case i <= dynf:
		return uint32(i)
	case i < 14:
		v := dynf + 1
		return uint32((i-v)*16 + uint16(gr.nyb()) + v)
	default:
		switch i {
		case 14:
			gr.repeat = gr.pknum()
		default:
			gr.repeat = 1
		}
		return gr.read()
	}
}

func (gr *glyphReader) rest() uint32 {
	switch {
	case gr.remainder < 0:
		gr.remainder = -gr.remainder
		return 0
	case gr.remainder > 0:
		switch {
		case gr.remainder > 4000:
			gr.remainder = 4000 - gr.remainder
			return 4000
		default:
			i := uint32(gr.remainder)
			gr.remainder = 0
			gr.read = gr.pknum
			return i
		}
	}
	panic("impossible")
}

func (gr *glyphReader) huge(i, k uint16) uint32 {
	var (
		j    = k
		dynf = int32(gr.dynf)
	)
	for i != 0 {
		j = (j << 4) + uint16(gr.nyb())
		i--
	}
	gr.remainder = int32(j) - 15 + (13-dynf)*16 + dynf
	gr.read = gr.rest
	return gr.rest()
}

func (gr *glyphReader) nyb() int16 {
	var v uint16
	switch gr.bitweight {
	case 0:
		gr.bitweight = 16
		gr.inputbyte = uint16(gr.pkbyte())
		v = gr.inputbyte >> 4
	default:
		gr.bitweight = 0
		v = gr.inputbyte & 15
	}
	return int16(v)
}

func (gr *glyphReader) getbit() bool {
	gr.bitweight >>= 1
	if gr.bitweight == 0 {
		gr.inputbyte = gr.pkbyte()
		gr.bitweight = 128
	}
	return gr.inputbyte&gr.bitweight != 0
}

A font/pkf/opcode.go => font/pkf/opcode.go +47 -0
@@ 0,0 1,47 @@
// 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 pkf

import "fmt"

// pkID is the version of the PK file format.
const pkID = 89

// opCode is a PK file format command identifier.
type opCode uint8

const (
	opXXX1 opCode = iota + 240 // Special command uint8-len large
	opXXX2                     // Special command uint16-len large
	opXXX3                     // Special command uint24-len large
	opXXX4                     // Special command uint32-len large
	opYYY                      // Special command 32b large
	opPost                     // Beginning of the postamble
	opNOP                      // no-op
	opPre                      // Beginning of the preamble
)

func (op opCode) cmd() Cmd {
	switch op {
	case opXXX1:
		return &CmdXXX1{}
	case opXXX2:
		return &CmdXXX2{}
	case opXXX3:
		return &CmdXXX3{}
	case opXXX4:
		return &CmdXXX4{}
	case opYYY:
		return &CmdYYY{}
	case opPost:
		return &CmdPost{}
	case opNOP:
		return &CmdNOP{}
	case opPre:
		return &CmdPre{}
	default:
		panic(fmt.Errorf("pkf: unknown opcode 0x%x (%d)", op, op))
	}
}

A font/pkf/pkf.go => font/pkf/pkf.go +6 -0
@@ 0,0 1,6 @@
// 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 pkf implements a decoder for the Packed (PK) Font file format.
package pkf // import "star-tex.org/x/tex/font/pkf"

A font/pkf/pkf_test.go => font/pkf/pkf_test.go +67 -0
@@ 0,0 1,67 @@
// 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 pkf

import (
	"testing"
)

func TestOpCode(t *testing.T) {
	for _, tc := range []struct {
		opcode opCode
		want   opCode
	}{
		{opXXX1, 240},
		{opXXX2, 241},
		{opXXX3, 242},
		{opXXX4, 243},
		{opYYY, 244},
		{opPost, 245},
		{opNOP, 246},
		{opPre, 247},
	} {
		t.Run(tc.opcode.cmd().Name(), func(t *testing.T) {
			if got, want := tc.opcode, tc.want; got != want {
				t.Fatalf("invalid opcode value: got=%d, want=%d", got, want)
			}
		})
	}
}

//func TestReader(t *testing.T) {
//	ctx := kpath.New()
//	for _, tc := range []struct {
//		name string
//		want string
//	}{
//		{
//			name: "fonts/pk/ljfour/public/cm/dpi600/cmr10.pk",
//			want: "xx",
//		},
//	} {
//		t.Run(tc.name, func(t *testing.T) {
//			f, err := ctx.Open(tc.name)
//			if err != nil {
//				t.Fatalf("could not open PK file: %+v", err)
//			}
//			defer f.Close()
//
//			r := NewReader(f)
//			err = r.Read(func(cmd Cmd) error {
//				log.Printf("cmd: %#v", cmd)
//				if cmd, ok := cmd.(*CmdPre); ok {
//					log.Printf("design: %d", cmd.Design)
//					log.Printf("chksum: %d", cmd.Checksum)
//					log.Printf("hppp: %d", cmd.Hppp)
//					log.Printf("vppp: %d", cmd.Vppp)
//				}
//				return nil
//			})
//			if err != nil {
//				t.Fatalf("could not read PK file: %+v", err)
//			}
//		})
//	}
//}