package caption import ( "image" "image/color" "syscall" "github.com/golang/freetype/truetype" "golang.org/x/image/draw" "golang.org/x/image/font" "golang.org/x/image/math/fixed" ) type Line struct { // collection of all text segments Data []Segment `json:"data"` // applies to all segments; avoid direct assignment and use SetFont instead Font []byte `json:"font"` // applies to all segments; avoid direct assignment and use SetFont instead Options *truetype.Options `json:"options"` // contains reference to parsed font, populated once per instance face font.Face // whether to fail if pixels are drawn out-of-bounds bounds bool } // SetFont clears line font cache and sets the new font. func (l *Line) SetFont(v []byte, opts *truetype.Options) { l.face = nil; l.Font = v; l.Options = opts } // SetBoundsCheck changes whether the frame's bounding box is enforced. func (l *Line) SetBoundsCheck(v bool) { l.bounds = v } type Segment struct { Value string `json:"value"` Color color.Color `json:"color"` } // Render draws the current contents of Line on [draw.Image]. func (l *Line) Render(ctx *Context, frame draw.Image, x, y int) error { face := l.face if face == nil { if len(l.Font) != 0 { // parse & cache Line font if err := l.cacheFont(l.Font); err != nil { return err } return l.Render(ctx, frame, x, y) } // fall back to default font if l.Options != nil { // global default: Options is not zero // cache global default without clobbering Line font if err := l.cacheFont(defaultFont); err != nil { return err // unreachable } return l.Render(ctx, frame, x, y) } // fast path: Font and Options are zero if err := ctx.parseFont(); err != nil { return err } face = ctx.defaultFont } outOfBounds := false bounds := frame.Bounds() boundsFixed := fixed.R(bounds.Min.X, bounds.Min.Y, bounds.Max.X, bounds.Max.Y) drawer := &font.Drawer{Dst: frame, Src: new(image.Uniform), Face: face, Dot: fixed.Point26_6{X: fixed.I(x), Y: fixed.I(y)}} for _, seg := range l.Data { drawer.Src.(*image.Uniform).C = seg.Color // optional bounds check to reduce overhead if l.bounds && !outOfBounds { b, _ := drawer.BoundString(seg.Value) outOfBounds = !b.In(boundsFixed) } drawer.DrawString(seg.Value) } if outOfBounds { return syscall.EDOM } return nil } // cacheFont parses truetype font data v and stores the result in Line. // The cache is invalidated by SetFont. func (l *Line) cacheFont(v []byte) error { f, err := truetype.Parse(v) if err == nil { l.face = truetype.NewFace(f, l.Options) } return err }