152 lines
5.3 KiB
Go
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 }
|