Allow rendering a text preview using custom fonts

This commit is contained in:
Juan Pablo Villafáñez committed 2021-11-25 15:42:53 +01:00
1 parent 54d450eddd
commit 38acfb1dbd
6 files changed
+620 -61

No files matched your search

+135 -52
View File
@@ -2,21 +2,17 @@ package preprocessor
import (
"bufio"
"github.com/golang/freetype"
"github.com/golang/freetype/truetype"
"github.com/pkg/errors"
"golang.org/x/image/font"
"golang.org/x/image/font/gofont/goregular"
"image"
"image/draw"
"io"
"math"
"mime"
"strings"
)
const (
fontSize = 12
spacing float64 = 1.5
"github.com/pkg/errors"
"golang.org/x/image/font"
"golang.org/x/image/font/opentype"
"golang.org/x/image/math/fixed"
)
type FileConverter interface {
@@ -33,73 +29,160 @@ func (i ImageDecoder) Convert(r io.Reader) (image.Image, error) {
return img, nil
}
type TxtToImageConverter struct{}
type TxtToImageConverter struct {
fontLoader *FontLoader
}
func (t TxtToImageConverter) Convert(r io.Reader) (image.Image, error) {
img := image.NewRGBA(image.Rect(0, 0, 640, 480))
draw.Draw(img, img.Bounds(), image.White, image.Point{}, draw.Src)
c := freetype.NewContext()
// Ignoring the error since we are using the embedded Golang font.
// This shouldn't return an error.
f, _ := truetype.Parse(goregular.TTF)
c.SetFont(f)
c.SetFontSize(fontSize)
c.SetClip(img.Bounds())
c.SetDst(img)
c.SetSrc(image.Black)
c.SetHinting(font.HintingFull)
pt := freetype.Pt(10, 10+int(c.PointToFixed(fontSize)>>6))
imgBounds := img.Bounds()
draw.Draw(img, imgBounds, image.White, image.Point{}, draw.Src)
fontSizeAsInt := int(math.Ceil(t.fontLoader.GetFaceOptSize()))
margin := 10
minX := fixed.I(imgBounds.Min.X + margin)
maxX := fixed.I(imgBounds.Max.X - margin)
maxY := fixed.I(imgBounds.Max.Y - margin)
initialPoint := fixed.P(imgBounds.Min.X+margin, imgBounds.Min.Y+margin+fontSizeAsInt)
canvas := &font.Drawer{
Dst: img,
Src: image.Black,
Dot: initialPoint,
}
scriptList := t.fontLoader.GetScriptList()
textAnalyzer := NewTextAnalyzer(scriptList)
taOpts := AnalysisOpts{
UseMergeMap: true,
MergeMap: DefaultMergeMap,
}
scanner := bufio.NewScanner(r)
Scan: // Label for the scanner loop, so we can break it easily
for scanner.Scan() {
txt := scanner.Text()
cs := chunks(txt, 80)
for _, s := range cs {
_, err := c.DrawString(strings.TrimSpace(s), pt)
if err != nil {
return nil, err
}
pt.Y += c.PointToFixed(fontSize * spacing)
if pt.Y.Round() >= img.Bounds().Dy() {
return img, scanner.Err()
height := fixed.I(fontSizeAsInt) // reset to default height
if txt != "" {
textResult := textAnalyzer.AnalyzeString(txt, taOpts)
textResult.MergeCommon(DefaultMergeMap)
for _, sRange := range textResult.ScriptRanges {
targetFontFace, _ := t.fontLoader.LoadFaceForScript(sRange.TargetScript)
// if the target script is "_unknown" it's expected that the loaded face
// uses the default font
faceHeight := targetFontFace.Face.Metrics().Height
if faceHeight > height {
height = faceHeight
}
canvas.Face = targetFontFace.Face
initialByte := sRange.Low
for _, sRangeSpace := range sRange.Spaces {
if canvas.Dot.Y > maxY {
break Scan
}
drawWord(canvas, textResult.Text[initialByte:sRangeSpace], minX, maxX, height, maxY, true)
initialByte = sRangeSpace
}
if initialByte <= sRange.High {
// some bytes left to be written
if canvas.Dot.Y > maxY {
break Scan
}
drawWord(canvas, textResult.Text[initialByte:sRange.High+1], minX, maxX, height, maxY, len(sRange.Spaces) > 0)
}
}
}
canvas.Dot.X = minX
canvas.Dot.Y += height.Mul(fixed.Int26_6(1<<6 + 1<<5)) // height * 1.5
if canvas.Dot.Y > maxY {
break
}
}
return img, scanner.Err()
}
// Code from https://stackoverflow.com/a/61469854
// Written By Igor Mikushkin
func chunks(s string, chunkSize int) []string {
if chunkSize >= len(s) {
return []string{s}
}
var chunks []string
chunk := make([]rune, chunkSize)
length := 0
for _, r := range s {
chunk[length] = r
length++
if length == chunkSize {
chunks = append(chunks, string(chunk))
length = 0
// Draw the word in the canvas. The mixX and maxX defines the drawable range
// (X axis) where the word can be drawn (in case the word is too big and doesn't
// fit in the canvas), and the incY defines the increment in the Y axis if we
// need to draw the word in a new line
//
// Note that the word will likely start with a white space char
func drawWord(canvas *font.Drawer, word string, minX, maxX, incY, maxY fixed.Int26_6, goToNewLine bool) {
bbox, _ := canvas.BoundString(word)
if bbox.Max.X <= maxX {
// word fits in the current line
canvas.DrawString(word)
} else {
// word doesn't fit -> retry in a new line
trimmedWord := strings.TrimSpace(word)
oldDot := canvas.Dot
canvas.Dot.X = minX
canvas.Dot.Y += incY
bbox2, _ := canvas.BoundString(trimmedWord)
if goToNewLine && bbox2.Max.X <= maxX {
if canvas.Dot.Y > maxY {
// Don't draw if we're over the Y limit
return
}
canvas.DrawString(trimmedWord)
} else {
// word doesn't fit in a new line -> draw as many chars as possible
canvas.Dot = oldDot
for _, char := range trimmedWord {
charBytes := []byte(string(char))
bbox3, _ := canvas.BoundBytes(charBytes)
if bbox3.Max.X > maxX {
canvas.Dot.X = minX
canvas.Dot.Y += incY
if canvas.Dot.Y > maxY {
// Don't draw if we're over the Y limit
return
}
}
canvas.DrawBytes(charBytes)
}
}
}
if length > 0 {
chunks = append(chunks, string(chunk[:length]))
}
return chunks
}
func ForType(mimeType string) FileConverter {
func ForType(mimeType string, opts map[string]interface{}) FileConverter {
// We can ignore the error here because we parse it in IsMimeTypeSupported before and if it fails
// return the service call. So we should only get here when the mimeType parses fine.
mimeType, _, _ = mime.ParseMediaType(mimeType)
switch mimeType {
case "text/plain":
return TxtToImageConverter{}
fontFileMap := ""
fontFaceOpts := &opentype.FaceOptions{
Size: 12,
DPI: 72,
Hinting: font.HintingNone,
}
if optedFontFileMap, ok := opts["fontFileMap"]; ok {
if stringFontFileMap, ok := optedFontFileMap.(string); ok {
fontFileMap = stringFontFileMap
}
}
if optedFontFaceOpts, ok := opts["fontFaceOpts"]; ok {
if typedFontFaceOpts, ok := optedFontFaceOpts.(*opentype.FaceOptions); ok {
fontFaceOpts = typedFontFaceOpts
}
}
fontLoader, err := NewFontLoader(fontFileMap, fontFaceOpts)
if err != nil {
// if couldn't create the FontLoader with the specified fontFileMap,
// try to use the default font
fontLoader, _ = NewFontLoader("", fontFaceOpts)
}
return TxtToImageConverter{
fontLoader: fontLoader,
}
default:
return ImageDecoder{}
}