star-tex dvi
| Language | Go |
|---|---|
| License | BSD-3-Clause |
| Repository | git.sr.ht/~sbinet/star-tex |
| Documentation | pkg.go.dev/star-tex.org/x/tex/dvi |
| Depends on | pure Go; TeX font metrics (TFM) for interpreting pages, found through star-tex.org/x/tex/kpath |
| Read / write | yes / yes (command level) |
star-tex is a port of TeX to Go, and its dvi package handles TeX’s output format. It works at two levels:
dvi.Dump(r, func(cmd dvi.Cmd) error)decodes the file into typed commands (*dvi.CmdPre,*dvi.CmdBOP,*dvi.CmdFntDef1,*dvi.CmdSetChar,*dvi.CmdXXX1, …). No fonts needed: good for inspecting files, counting pages, listing fonts and specials.dvi.Compile+dvi.NewMachine(...).Runexecutes the page program like a DVI driver, tracking positions and fonts, and calls advi.RendererwithBOP,EOP,DrawGlyphandDrawRule. This needs the TFM files of the fonts, found with akpath.Context(a TeX tree on disk or the minimal tree built in).
The commands also have a write side used by star-tex itself, so DVI can be produced. It’s a young project (v0.7.x); the API may change between minor versions.
Installation#
go get star-tex.org/x/tex@latest
go run star-tex.org/x/tex/cmd/dvi-dump@latest file.dvi prints a DVI file in readable form.
Basic usage: count pages and list fonts#
package main
import (
"fmt"
"log"
"os"
"star-tex.org/x/tex/dvi"
)
func main() {
f, err := os.Open("paper.dvi")
if err != nil {
log.Fatal(err)
}
defer f.Close()
pages := 0
err = dvi.Dump(f, func(cmd dvi.Cmd) error {
switch c := cmd.(type) {
case *dvi.CmdPre:
fmt.Printf("DVI version %d, comment %q\n", c.Version, c.Msg)
case *dvi.CmdBOP:
pages++
case *dvi.CmdFntDef1:
fmt.Printf("font %d: %s\n", c.ID, c.Font)
case *dvi.CmdPost:
fmt.Printf("postamble: %d pages, max size %d x %d sp\n", c.Pages, c.Width, c.Height)
}
return nil
})
if err != nil {
log.Fatal(err)
}
fmt.Println("pages:", pages)
}
Font definitions appear twice: before first use and again in the postamble.
How to list the \special commands in a DVI file#
Specials carry colors, image references and hyperlinks:
err = dvi.Dump(f, func(cmd dvi.Cmd) error {
if c, ok := cmd.(*dvi.CmdXXX1); ok {
fmt.Printf("special: %s\n", c.Value)
}
return nil
})
Long specials use *dvi.CmdXXX4.
How to get glyph and rule positions with a renderer#
Implement dvi.Renderer and run the file on a machine. Positions are in DVI units (scaled points: 65536 per TeX point), with y going down from the top of the page:
package main
import (
"fmt"
"image/color"
"log"
"os"
"star-tex.org/x/tex/dvi"
"star-tex.org/x/tex/kpath"
)
type printer struct{ page int }
func (p *printer) Init(pre *dvi.CmdPre, post *dvi.CmdPost) {}
func (p *printer) BOP(bop *dvi.CmdBOP) { p.page++; fmt.Println("page", bop.C0) }
func (p *printer) EOP() {}
func (p *printer) DrawGlyph(x, y int32, font dvi.Font, glyph rune, c color.Color) {
fmt.Printf(" %s char %d at (%.1f, %.1f) pt\n", font.Name(), glyph,
float64(x)/65536, float64(y)/65536)
}
func (p *printer) DrawRule(x, y, w, h int32, c color.Color) {
fmt.Printf(" rule %.1f x %.1f pt\n", float64(w)/65536, float64(h)/65536)
}
func main() {
raw, err := os.ReadFile("paper.dvi")
if err != nil {
log.Fatal(err)
}
prog, err := dvi.Compile(raw)
if err != nil {
log.Fatal(err)
}
// TeX tree with the TFM files; kpath.New() uses a minimal built-in tree
ktx, err := kpath.NewFromFS(os.DirFS("/usr/share/texlive/texmf-dist"))
if err != nil {
log.Fatal(err)
}
vm := dvi.NewMachine(dvi.WithContext(ktx), dvi.WithRenderer(&printer{}))
if err := vm.Run(prog); err != nil {
log.Fatal(err)
}
}
dvi.WithHandlers(dvi.NewColorHandler(ktx)) makes the machine follow color push/color pop specials, and the color is passed to DrawGlyph.
Tips#
kpath.NewFromFSuses thels-Rdatabase at the root of the tree if there is one, and otherwise walks the whole tree, which is slow on a full TeX Live.dvi.Dumpreads the whole file into memory; DVI files are small, so that’s rarely a problem.