From 01f50749024c419b3b1800893d74a02994b9ecbb Mon Sep 17 00:00:00 2001 From: Sebastien Binet Date: Wed, 31 Mar 2021 12:00:46 +0200 Subject: [PATCH] {cmd/pk2bm,font/pkf}: first import Fixes #8. Signed-off-by: Sebastien Binet --- cmd/pk2bm/main.go | 60 ++++++ cmd/pk2bm/main_test.go | 169 +++++++++++++++++ font/pkf/bitmap.go | 145 ++++++++++++++ font/pkf/cmd.go | 125 ++++++++++++ font/pkf/face.go | 242 ++++++++++++++++++++++++ font/pkf/font.go | 145 ++++++++++++++ font/pkf/font_test.go | 59 ++++++ font/pkf/glyph.go | 420 +++++++++++++++++++++++++++++++++++++++++ font/pkf/opcode.go | 47 +++++ font/pkf/pkf.go | 6 + font/pkf/pkf_test.go | 67 +++++++ 11 files changed, 1485 insertions(+) create mode 100644 cmd/pk2bm/main.go create mode 100644 cmd/pk2bm/main_test.go create mode 100644 font/pkf/bitmap.go create mode 100644 font/pkf/cmd.go create mode 100644 font/pkf/face.go create mode 100644 font/pkf/font.go create mode 100644 font/pkf/font_test.go create mode 100644 font/pkf/glyph.go create mode 100644 font/pkf/opcode.go create mode 100644 font/pkf/pkf.go create mode 100644 font/pkf/pkf_test.go diff --git a/cmd/pk2bm/main.go b/cmd/pk2bm/main.go new file mode 100644 index 0000000..7cfef38 --- /dev/null +++ b/cmd/pk2bm/main.go @@ -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) + } +} diff --git a/cmd/pk2bm/main_test.go b/cmd/pk2bm/main_test.go new file mode 100644 index 0000000..5840c47 --- /dev/null +++ b/cmd/pk2bm/main_test.go @@ -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 + + ...........********................... + ........**************................ + ......*****.......******.............. + .....***............*****............. + ....*****............******........... + ...*******............******.......... + ...********...........******.......... + ...********............******......... + ...********............******......... + ...********.............******........ + ....******..............******........ + .....****...............******........ + ........................******........ + ........................******........ + ........................******........ + ........................******........ + .................*************........ + .............*****************........ + ..........*********.....******........ + ........*******.........******........ + ......*******...........******........ + ....********............******........ + ...*******..............******........ + ..********..............******........ + .********...............******........ + .*******................******........ + .*******................******......** + *******.................******......** + *******.................******......** + *******.................******......** + *******................*******......** + *******................*******......** + ********..............********......** + .*******.............***.*****......** + .********............**...*****....**. + ..********.........****...*****....**. + ....*******......****......*********.. + ......**************........*******... + .........********............*****.... +` diff --git a/font/pkf/bitmap.go b/font/pkf/bitmap.go new file mode 100644 index 0000000..5b5a3d9 --- /dev/null +++ b/font/pkf/bitmap.go @@ -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 +} diff --git a/font/pkf/cmd.go b/font/pkf/cmd.go new file mode 100644 index 0000000..a6a8c1f --- /dev/null +++ b/font/pkf/cmd.go @@ -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) +) diff --git a/font/pkf/face.go b/font/pkf/face.go new file mode 100644 index 0000000..bac8444 --- /dev/null +++ b/font/pkf/face.go @@ -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) +) diff --git a/font/pkf/font.go b/font/pkf/font.go new file mode 100644 index 0000000..765d4fa --- /dev/null +++ b/font/pkf/font.go @@ -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 +} diff --git a/font/pkf/font_test.go b/font/pkf/font_test.go new file mode 100644 index 0000000..5d0f4f4 --- /dev/null +++ b/font/pkf/font_test.go @@ -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() + } + }) + } +} diff --git a/font/pkf/glyph.go b/font/pkf/glyph.go new file mode 100644 index 0000000..871043a --- /dev/null +++ b/font/pkf/glyph.go @@ -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 +} diff --git a/font/pkf/opcode.go b/font/pkf/opcode.go new file mode 100644 index 0000000..bf0faf3 --- /dev/null +++ b/font/pkf/opcode.go @@ -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)) + } +} diff --git a/font/pkf/pkf.go b/font/pkf/pkf.go new file mode 100644 index 0000000..af46f71 --- /dev/null +++ b/font/pkf/pkf.go @@ -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" diff --git a/font/pkf/pkf_test.go b/font/pkf/pkf_test.go new file mode 100644 index 0000000..e8453dc --- /dev/null +++ b/font/pkf/pkf_test.go @@ -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) +// } +// }) +// } +//} -- 2.34.2