mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-10-23 06:39:13 -04:00
Allow rendering a text preview using custom fonts
This commit is contained in:
1 parent
54d450eddd
commit
38acfb1dbd
6 files changed
+620
-61
No files matched your search
@@ -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{}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user