| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360 |
- // The original code in this file comes from
- //
- // https://git.sr.ht/~sbinet/star-tex/tree/main/item/cmd/dvi-cnv
- //
- // and is
- //
- // 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-STAR-TEX file.
- //
- // Modifications are
- //
- // Copyright 2024 The tk9.0-go 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 tk9_0 // import "modernc.org/tk9.0"
- import (
- "bytes"
- "fmt"
- "image"
- "image/color"
- "image/draw"
- "image/png"
- "io"
- "strings"
- "github.com/disintegration/imaging"
- "golang.org/x/image/font"
- fixedmath "golang.org/x/image/math/fixed"
- "modernc.org/knuth/dvi"
- "modernc.org/knuth/font/fixed"
- "modernc.org/knuth/font/pkf"
- "modernc.org/knuth/kpath"
- "modernc.org/knuth/tex"
- )
- const (
- shrink = 1
- )
- var (
- _ dvi.Renderer = (*renderer)(nil)
- )
- type fntkey struct {
- name string
- size fixed.Int12_20
- }
- type renderer struct {
- bkg color.Color
- bound image.Rectangle
- ctx kpath.Context
- err error
- faces map[fntkey]font.Face
- final image.Image
- img *image.RGBA
- page int
- post dvi.CmdPost
- pre dvi.CmdPre
- scale float64
- conv float32 // converts DVI units to pixels
- dpi float32
- tconv float32 // converts unmagnified DVI units to pixels
- bounded bool
- }
- func newRenderer(ctx kpath.Context, scale float64) *renderer {
- return &renderer{ctx: ctx, faces: make(map[fntkey]font.Face), scale: scale}
- }
- func (pr *renderer) Init(pre *dvi.CmdPre, post *dvi.CmdPost) {
- pr.pre = *pre
- pr.post = *post
- if pr.dpi == 0 {
- pr.dpi = 600
- }
- res := pr.dpi
- conv := float32(pr.pre.Num) / 254000.0 * (res / float32(pr.pre.Den))
- pr.tconv = conv
- pr.conv = conv * float32(pr.pre.Mag) / 1000.0
- conv = 1/(float32(pre.Num)/float32(pre.Den)*(float32(pre.Mag)/1000.0)*(pr.dpi*shrink/254000.0)) + 0.5
- if pr.bkg == nil {
- pr.bkg = color.Transparent
- }
- }
- func (pr *renderer) BOP(bop *dvi.CmdBOP) {
- if pr.err != nil {
- return
- }
- pr.page = int(bop.C0)
- bnd := image.Rect(0, 0, int(pr.pixels(int32(pr.post.Width))), int(pr.pixels(int32(pr.post.Height))))
- pr.img = image.NewRGBA(bnd)
- draw.Draw(pr.img, bnd, image.NewUniform(pr.bkg), image.Point{}, draw.Over)
- }
- func (pr *renderer) DrawGlyph(x, y int32, font dvi.Font, glyph rune, c color.Color) {
- if pr.err != nil {
- return
- }
- dot := fixedmath.Point26_6{X: fixedmath.I(int(pr.pixels(x))), Y: fixedmath.I(int(pr.pixels(y)))}
- face, ok := pr.face(font)
- if !ok {
- return
- }
- dr, mask, maskp, _, ok := face.Glyph(dot, glyph)
- if !ok {
- pr.setErr(fmt.Errorf("could not find glyph 0x%02x", glyph))
- return
- }
- draw.DrawMask(pr.img, dr, image.NewUniform(c), image.Point{}, mask, maskp, draw.Over)
- pr.union(dr)
- }
- func (pr *renderer) union(r image.Rectangle) {
- switch pr.bounded {
- case true:
- pr.bound = pr.bound.Union(r)
- default:
- pr.bound = r
- pr.bounded = true
- }
- }
- func (pr *renderer) DrawRule(x, y, w, h int32, c color.Color) {
- if pr.err != nil {
- return
- }
- r := image.Rect(int(pr.pixels(x)), int(pr.pixels(y)), int(pr.pixels(x+w)), int(pr.pixels(y-h)))
- draw.Draw(pr.img, r, image.NewUniform(c), image.Point{}, draw.Over)
- pr.union(r)
- }
- func (pr *renderer) EOP() {
- if pr.err != nil {
- return
- }
- pr.final = pr.img.SubImage(pr.bound)
- if pr.scale != 1.0 {
- pr.final = imaging.Resize(pr.final, int(float64(pr.bound.Max.X-pr.bound.Min.X)*pr.scale+0.5), 0, imaging.Lanczos)
- }
- }
- func (pr *renderer) setErr(err error) {
- if pr.err == nil {
- pr.err = err
- }
- }
- func (pr *renderer) face(fnt dvi.Font) (font.Face, bool) {
- key := fntkey{
- name: fnt.Name(),
- size: fnt.Size(),
- }
- if f, ok := pr.faces[key]; ok {
- return f, ok
- }
- fname, err := pr.ctx.Find(key.name + ".pk")
- if err != nil {
- pr.setErr(fmt.Errorf("could not find font face %q: %+v", key.name, err))
- return nil, false
- }
- f, err := pr.ctx.Open(fname)
- if err != nil {
- pr.setErr(fmt.Errorf("could not open font face %q: %+v", key.name, err))
- return nil, false
- }
- defer f.Close()
- pk, err := pkf.Parse(f)
- if err != nil {
- pr.setErr(fmt.Errorf("could not parse font face %q: %+v", key.name, err))
- return nil, false
- }
- tfm := fnt.Metrics()
- if tfm.Checksum() != pk.Checksum() {
- pr.setErr(fmt.Errorf(
- "TFM and PK checksum do not match for %q: tfm=0x%x, pk=0x%x",
- key.name,
- tfm.Checksum(),
- pk.Checksum(),
- ))
- return nil, false
- }
- face := pkf.NewFace(pk, tfm, &pkf.FaceOptions{
- Size: tfm.DesignSize().Float64(),
- DPI: float64(pr.dpi),
- })
- pr.faces[key] = face
- return face, true
- }
- func (pr *renderer) pixels(v int32) int32 {
- x := pr.conv * float32(v)
- return roundF32(x / shrink)
- }
- func roundF32(v float32) int32 {
- if v > 0 {
- return int32(v + 0.5)
- }
- return int32(v - 0.5)
- }
- func dvi2png(r io.Reader, scale float64) (_ []byte, err error) {
- img, err := dvi2img(r, scale)
- if err != nil {
- return nil, err
- }
- var out bytes.Buffer
- if err = png.Encode(&out, img); err != nil {
- return nil, err
- }
- return out.Bytes(), nil
- }
- func dvi2img(r io.Reader, scale float64) (img image.Image, err error) {
- ctx := kpath.New()
- renderer := newRenderer(ctx, scale)
- vm := dvi.NewMachine(
- dvi.WithContext(ctx),
- dvi.WithRenderer(renderer),
- dvi.WithHandlers(dvi.NewColorHandler(ctx)),
- dvi.WithOffsetX(0),
- dvi.WithOffsetY(0),
- )
- raw, err := io.ReadAll(r)
- if err != nil {
- return nil, fmt.Errorf("could not read DVI program file: %w", err)
- }
- prog, err := dvi.Compile(raw)
- if err != nil {
- return nil, fmt.Errorf("could not compile DVI program: %w", err)
- }
- err = vm.Run(prog)
- if err != nil {
- return nil, fmt.Errorf("could not interpret DVI program: %w", err)
- }
- if renderer.err != nil {
- return nil, fmt.Errorf("could not render DVI program: %w", renderer.err)
- }
- return renderer.final, nil
- }
- func tex2dvi(src string) (dvi *bytes.Buffer, err error) {
- // To get rid of the page number rendered by default, the function prepends
- // "\footline={}\n" to src. Also, "\n\bye\n" is appended to 'src' to make it a
- // complete TeX document.
- var stdout, stderr, b bytes.Buffer
- nm := wmTitle
- switch {
- case nm == "plain":
- nm += "_"
- case nm == "":
- nm = "x"
- }
- if err = tex.Main(
- strings.NewReader(fmt.Sprintf("\\input plain \\input %s", nm)),
- &stdout,
- &stderr,
- tex.WithInputFile(nm+".tex", strings.NewReader(fmt.Sprintf("\\footline={}\n%s\n\\bye\n", src))),
- tex.WithDVIFile(&b),
- tex.WithLogFile(io.Discard),
- ); err != nil {
- a := []string{err.Error()}
- if b := stdout.Bytes(); len(b) != 0 {
- a = append(a, string(b))
- }
- if b := stderr.Bytes(); len(b) != 0 {
- a = append(a, string(b))
- }
- return nil, fmt.Errorf("%s", strings.Join(a, "\n"))
- }
- return &b, nil
- }
- func sanitizeTeX(s string) (r string) {
- s = strings.TrimSpace(s)
- a := strings.Fields(s)
- return strings.Join(a, " ")
- }
- // TeX is like Tex2 but report errors using [ErrorMode] and [Error].
- func TeX(src string, scale float64) (png []byte) {
- b, err := TeX2(src, scale)
- if err != nil {
- fail(err)
- return nil
- }
- return b
- }
- // TeX2 renders TeX 'src' as a png file that shows the TeX "snippet" in a fixed
- // 600 dpi resolution. The result is afterwards resized using the 'scale'
- // factor. Scale factor 1.0 means no resize.
- //
- // Only plain Tex and a subset of some of the default Computer Modern fonts are
- // supported. Many small fonts are not available.
- func TeX2(src string, scale float64) (png []byte, err error) {
- if src = sanitizeTeX(src); src == "" {
- return nil, fmt.Errorf("empty TeX code")
- }
- dvi, err := tex2dvi(src)
- if err != nil {
- return nil, err
- }
- return dvi2png(dvi, scale)
- }
- // TeXImg renders is line TeX but returns an [image.Image].
- func TeXImg(src string, scale float64) (img image.Image) {
- img, err := TeXImg2(src, scale)
- if err != nil {
- fail(err)
- return nil
- }
- return img
- }
- // TeXImg2 renders is line TeX2 but returns an [image.Image].
- func TeXImg2(src string, scale float64) (img image.Image, err error) {
- if src = sanitizeTeX(src); src == "" {
- return nil, fmt.Errorf("empty TeX code")
- }
- dvi, err := tex2dvi(src)
- if err != nil {
- return nil, err
- }
- return dvi2img(dvi, scale)
- }
|