adam-gui/vendor/github.com/go-text/render/render.go
2024-04-29 19:13:50 +02:00

152 lines
5.3 KiB
Go

package render
import (
"image/color"
"image/draw"
"math"
"github.com/go-text/typesetting/font"
"github.com/go-text/typesetting/opentype/api"
"github.com/go-text/typesetting/shaping"
"github.com/srwiley/rasterx"
"golang.org/x/image/math/fixed"
)
// Renderer defines a type that can render strings to a bitmap canvas.
// The size and look of output depends on the various fields in this struct.
// Developers should provide suitable output images for their draw requests.
// This type is not thread safe so instances should be used from only 1 goroutine.
type Renderer struct {
// FontSize defines the point size of output text, commonly between 10 and 14 for regular text
FontSize float32
// PixScale is used to indicate the pixel density of your output target.
// For example on a hi-DPI (or "retina") display this may be 2.0.
// Default value is 1.0, meaning 1 pixel on the image for each render pixel.
PixScale float32
// Color is the pen colour for rendering
Color color.Color
segmenter shaping.Segmenter
shaper shaping.HarfbuzzShaper
filler *rasterx.Filler
fillerScale float32
}
func (r *Renderer) shape(str string, face font.Face) (_ shaping.Line, ascent int) {
text := []rune(str)
in := shaping.Input{
Text: text,
RunStart: 0,
RunEnd: len(text),
Face: face,
Size: fixed.I(int(r.FontSize)),
}
runs := r.segmenter.Split(in, singleFontMap{face})
line := make(shaping.Line, len(runs))
for i, run := range runs {
line[i] = r.shaper.Shape(run)
if a := line[i].LineBounds.Ascent.Ceil(); a > ascent {
ascent = a
}
}
return line, ascent
}
// DrawString will rasterise the given string into the output image using the specified font face.
// The text will be drawn starting at the left edge, down from the image top by the
// font ascent value, so that the text is all visible.
// The return value is the X pixel position of the end of the drawn string.
func (r *Renderer) DrawString(str string, img draw.Image, face font.Face) int {
line, ascent := r.shape(str, face)
x := 0
for _, run := range line {
x = r.DrawShapedRunAt(run, img, x, ascent)
}
return x
}
// DrawStringAt will rasterise the given string into the output image using the specified font face.
// The text will be drawn starting at the x, y pixel position.
// Note that x and y are not multiplied by the `PixScale` value as they refer to output coordinates.
// The return value is the X pixel position of the end of the drawn string.
func (r *Renderer) DrawStringAt(str string, img draw.Image, x, y int, face font.Face) int {
line, _ := r.shape(str, face)
for _, run := range line {
x = r.DrawShapedRunAt(run, img, x, y)
}
return x
}
// DrawShapedRunAt will rasterise the given shaper run into the output image using font face referenced in the shaping.
// The text will be drawn starting at the startX, startY pixel position.
// Note that startX and startY are not multiplied by the `PixScale` value as they refer to output coordinates.
// The return value is the X pixel position of the end of the drawn string.
func (r *Renderer) DrawShapedRunAt(run shaping.Output, img draw.Image, startX, startY int) int {
if r.PixScale == 0 {
r.PixScale = 1
}
scale := r.FontSize * r.PixScale / float32(run.Face.Upem())
r.fillerScale = scale
b := img.Bounds()
scanner := rasterx.NewScannerGV(b.Dx(), b.Dy(), img, b)
f := rasterx.NewFiller(b.Dx(), b.Dy(), scanner)
r.filler = f
f.SetColor(r.Color)
x := float32(startX)
y := float32(startY)
for _, g := range run.Glyphs {
xPos := x + fixed266ToFloat(g.XOffset)*r.PixScale
yPos := y - fixed266ToFloat(g.YOffset)*r.PixScale
data := run.Face.GlyphData(g.GlyphID)
switch format := data.(type) {
case api.GlyphOutline:
r.drawOutline(g, format, f, scale, xPos, yPos)
case api.GlyphBitmap:
_ = r.drawBitmap(g, format, img, xPos, yPos)
case api.GlyphSVG:
_ = r.drawSVG(g, format, img, xPos, yPos)
}
x += fixed266ToFloat(g.XAdvance) * r.PixScale
}
f.Draw()
r.filler = nil
return int(math.Ceil(float64(x)))
}
func (r *Renderer) drawOutline(g shaping.Glyph, bitmap api.GlyphOutline, f *rasterx.Filler, scale float32, x, y float32) {
for _, s := range bitmap.Segments {
switch s.Op {
case api.SegmentOpMoveTo:
f.Start(fixed.Point26_6{X: floatToFixed266(s.Args[0].X*scale + x), Y: floatToFixed266(-s.Args[0].Y*scale + y)})
case api.SegmentOpLineTo:
f.Line(fixed.Point26_6{X: floatToFixed266(s.Args[0].X*scale + x), Y: floatToFixed266(-s.Args[0].Y*scale + y)})
case api.SegmentOpQuadTo:
f.QuadBezier(fixed.Point26_6{X: floatToFixed266(s.Args[0].X*scale + x), Y: floatToFixed266(-s.Args[0].Y*scale + y)},
fixed.Point26_6{X: floatToFixed266(s.Args[1].X*scale + x), Y: floatToFixed266(-s.Args[1].Y*scale + y)})
case api.SegmentOpCubeTo:
f.CubeBezier(fixed.Point26_6{X: floatToFixed266(s.Args[0].X*scale + x), Y: floatToFixed266(-s.Args[0].Y*scale + y)},
fixed.Point26_6{X: floatToFixed266(s.Args[1].X*scale + x), Y: floatToFixed266(-s.Args[1].Y*scale + y)},
fixed.Point26_6{X: floatToFixed266(s.Args[2].X*scale + x), Y: floatToFixed266(-s.Args[2].Y*scale + y)})
}
}
f.Stop(true)
}
func fixed266ToFloat(i fixed.Int26_6) float32 {
return float32(float64(i) / 64)
}
func floatToFixed266(f float32) fixed.Int26_6 {
return fixed.Int26_6(int(float64(f) * 64))
}
type singleFontMap struct {
face font.Face
}
func (sf singleFontMap) ResolveFace(rune) font.Face { return sf.face }