successfully mixing in a normal font + the emoji font for some runes only through a myriad of weird hacks.

This commit is contained in:
fiatjaf
2023-12-26 22:08:24 -03:00
parent 9be417fc7c
commit 1022fb76fa
3 changed files with 140 additions and 62 deletions

View File

@@ -13,14 +13,16 @@ import (
"sync" "sync"
"time" "time"
"github.com/dmolesUC3/emoji"
"github.com/fogleman/gg" "github.com/fogleman/gg"
"github.com/go-text/typesetting/di" "github.com/go-text/typesetting/di"
"github.com/go-text/typesetting/font" "github.com/go-text/typesetting/font"
"github.com/go-text/typesetting/harfbuzz" "github.com/go-text/typesetting/harfbuzz"
"github.com/go-text/typesetting/language" "github.com/go-text/typesetting/language"
"github.com/go-text/typesetting/opentype/api"
"github.com/go-text/typesetting/shaping" "github.com/go-text/typesetting/shaping"
"github.com/pemistahl/lingua-go" "github.com/pemistahl/lingua-go"
"github.com/puzpuzpuz/xsync/v2" "github.com/srwiley/rasterx"
"golang.org/x/image/math/fixed" "golang.org/x/image/math/fixed"
) )
@@ -85,7 +87,8 @@ var (
shaperLock sync.Mutex shaperLock sync.Mutex
shaperBuffer = harfbuzz.NewBuffer() shaperBuffer = harfbuzz.NewBuffer()
fontCache = xsync.NewTypedMapOf[font.Face, *harfbuzz.Font](pointerHasher) emojiBuffer = harfbuzz.NewBuffer()
fontCache = make(map[font.Face]*harfbuzz.Font)
emojiFont *harfbuzz.Font emojiFont *harfbuzz.Font
) )
@@ -342,59 +345,98 @@ func shortenURLs(text string) string {
}) })
} }
func shapeText(input shaping.Input) shaping.Output { func shapeText(rawText []rune, fontSize int) (shaping.Output, []bool) {
lang, script, dir, face := getLanguageAndScriptAndDirectionAndFont(rawText)
shaperLock.Lock() shaperLock.Lock()
defer shaperLock.Unlock() defer shaperLock.Unlock()
shaperBuffer.Clear()
runes, start, end := input.Text, input.RunStart, input.RunEnd // load or get main font from cache
if end < start { hfont, ok := fontCache[face]
panic("end < start") if !ok {
hfont = harfbuzz.NewFont(face)
fontCache[face] = hfont
} }
start = clamp(start, 0, len(runes))
end = clamp(end, 0, len(runes))
shaperBuffer.AddRunes(runes, start, end-start)
shaperBuffer.Props.Direction = input.Direction.Harfbuzz() // define this only once
shaperBuffer.Props.Language = input.Language input := shaping.Input{
shaperBuffer.Props.Script = input.Script Text: rawText,
RunStart: 0,
RunEnd: len(rawText),
Face: face,
Size: fixed.I(int(fontSize)),
Script: script,
Language: lang,
Direction: dir,
}
// load or get font from cache // shape stuff for both normal text and emojis
hfont, _ := fontCache.LoadOrCompute(input.Face, func() *harfbuzz.Font { for _, params := range []struct {
return harfbuzz.NewFont(input.Face) font *harfbuzz.Font
}) buf *harfbuzz.Buffer
}{
{hfont, shaperBuffer},
{emojiFont, emojiBuffer},
} {
params.buf.Clear() // clear before using
// adjust the user provided fields runes, start, end := input.Text, input.RunStart, input.RunEnd
hfont.XScale = int32(input.Size.Ceil()) << scaleShift if end < start {
hfont.YScale = hfont.XScale panic("end < start")
}
start = clamp(start, 0, len(runes))
end = clamp(end, 0, len(runes))
params.buf.AddRunes(runes, start, end-start)
// actually use harfbuzz to shape the text. params.buf.Props.Direction = input.Direction.Harfbuzz()
shaperBuffer.Shape(hfont, nil) params.buf.Props.Language = input.Language
params.buf.Props.Script = input.Script
// adjust the user provided fields
params.font.XScale = int32(input.Size.Ceil()) << scaleShift
params.font.YScale = params.font.XScale
// actually use harfbuzz to shape the text.
params.buf.Shape(params.font, nil)
}
// this will be used to determine whether a given glyph is an emoji or not when rendering
emojiMask := make([]bool, len(shaperBuffer.Info))
// convert the shaped text into an output // convert the shaped text into an output
glyphs := make([]shaping.Glyph, len(shaperBuffer.Info)) glyphs := make([]shaping.Glyph, len(shaperBuffer.Info))
for i := range glyphs { for i := 0; i < len(glyphs); i++ {
g := shaperBuffer.Info[i].Glyph var buf *harfbuzz.Buffer
glyphs[i] = shaping.Glyph{ var font *harfbuzz.Font
ClusterIndex: shaperBuffer.Info[i].Cluster, if emoji.IsEmoji(rawText[i]) {
GlyphID: g, buf = emojiBuffer
Mask: shaperBuffer.Info[i].Mask, font = emojiFont
emojiMask[i] = true
} else {
buf = shaperBuffer
font = hfont
} }
extents, ok := hfont.GlyphExtents(g) glyph := buf.Info[i]
glyphs[i] = shaping.Glyph{
ClusterIndex: glyph.Cluster,
GlyphID: glyph.Glyph,
Mask: glyph.Mask,
}
extents, ok := font.GlyphExtents(glyph.Glyph)
if !ok { if !ok {
// leave the glyph having zero size if it isn't in the font. There
// isn't really anything we can do to recover from such an error.
continue continue
} }
glyphs[i].Width = fixed.I(int(extents.Width)) >> scaleShift glyphs[i].Width = fixed.I(int(extents.Width)) >> scaleShift
glyphs[i].Height = fixed.I(int(extents.Height)) >> scaleShift glyphs[i].Height = fixed.I(int(extents.Height)) >> scaleShift
glyphs[i].XBearing = fixed.I(int(extents.XBearing)) >> scaleShift glyphs[i].XBearing = fixed.I(int(extents.XBearing)) >> scaleShift
glyphs[i].YBearing = fixed.I(int(extents.YBearing)) >> scaleShift glyphs[i].YBearing = fixed.I(int(extents.YBearing)) >> scaleShift
glyphs[i].XAdvance = fixed.I(int(shaperBuffer.Pos[i].XAdvance)) >> scaleShift glyphs[i].XAdvance = fixed.I(int(buf.Pos[i].XAdvance)) >> scaleShift
glyphs[i].YAdvance = fixed.I(int(shaperBuffer.Pos[i].YAdvance)) >> scaleShift glyphs[i].YAdvance = fixed.I(int(buf.Pos[i].YAdvance)) >> scaleShift
glyphs[i].XOffset = fixed.I(int(shaperBuffer.Pos[i].XOffset)) >> scaleShift glyphs[i].XOffset = fixed.I(int(buf.Pos[i].XOffset)) >> scaleShift
glyphs[i].YOffset = fixed.I(int(shaperBuffer.Pos[i].YOffset)) >> scaleShift glyphs[i].YOffset = fixed.I(int(buf.Pos[i].YOffset)) >> scaleShift
} }
countClusters(glyphs, input.RunEnd, input.Direction.Progression()) countClusters(glyphs, input.RunEnd, input.Direction.Progression())
out := shaping.Output{ out := shaping.Output{
Glyphs: glyphs, Glyphs: glyphs,
@@ -412,7 +454,8 @@ func shapeText(input shaping.Input) shaping.Output {
Gap: fixed.I(int(fontExtents.LineGap)) >> scaleShift, Gap: fixed.I(int(fontExtents.LineGap)) >> scaleShift,
} }
out.RecalculateAll() out.RecalculateAll()
return out
return out, emojiMask
} }
// countClusters tallies the number of runes and glyphs in each cluster // countClusters tallies the number of runes and glyphs in each cluster
@@ -455,3 +498,61 @@ func countClusters(glyphs []shaping.Glyph, textLen int, dir di.Progression) {
glyphs[i].RuneCount = runesInCluster glyphs[i].RuneCount = runesInCluster
} }
} }
func drawShapedRunAt(fontSize int, clr color.Color, out shaping.Output, emojiMask []bool, img draw.Image, startX, startY int) int {
b := img.Bounds()
scanner := rasterx.NewScannerGV(b.Dx(), b.Dy(), img, b)
f := rasterx.NewFiller(b.Dx(), b.Dy(), scanner)
f.SetColor(clr)
x := float32(startX)
y := float32(startY)
for i, g := range out.Glyphs {
xPos := x + fixed266ToFloat(g.XOffset)
yPos := y - fixed266ToFloat(g.YOffset)
face := out.Face
if emojiMask[i] {
face = emojiFace
}
data := face.GlyphData(g.GlyphID)
switch format := data.(type) {
case api.GlyphOutline:
scale := float32(fontSize) / float32(face.Upem())
drawOutline(g, format, f, scale, xPos, yPos)
default:
panic("format not supported for glyph")
}
x += fixed266ToFloat(g.XAdvance)
}
f.Draw()
return int(math.Ceil(float64(x)))
}
func 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))
}

View File

@@ -13,13 +13,11 @@ import (
"time" "time"
"github.com/fogleman/gg" "github.com/fogleman/gg"
"github.com/go-text/render"
"github.com/go-text/typesetting/shaping" "github.com/go-text/typesetting/shaping"
"github.com/golang/freetype/truetype" "github.com/golang/freetype/truetype"
sdk "github.com/nbd-wtf/nostr-sdk" sdk "github.com/nbd-wtf/nostr-sdk"
"github.com/nfnt/resize" "github.com/nfnt/resize"
xfont "golang.org/x/image/font" xfont "golang.org/x/image/font"
"golang.org/x/image/math/fixed"
) )
const ( const (
@@ -173,11 +171,7 @@ func drawImage(paragraphs []string, style Style, metadata sdk.ProfileMetadata, d
func drawText(paragraphs []string, width, height int) image.Image { func drawText(paragraphs []string, width, height int) image.Image {
const FONT_SIZE = 25 const FONT_SIZE = 25
r := &render.Renderer{ color := color.RGBA{R: 255, G: 230, B: 238, A: 255}
PixScale: 1,
FontSize: FONT_SIZE,
Color: color.RGBA{R: 255, G: 230, B: 238, A: 255},
}
img := image.NewNRGBA(image.Rect(0, 0, width, height)) img := image.NewNRGBA(image.Rect(0, 0, width, height))
@@ -185,18 +179,7 @@ func drawText(paragraphs []string, width, height int) image.Image {
for _, paragraph := range paragraphs { for _, paragraph := range paragraphs {
rawText := []rune(paragraph) rawText := []rune(paragraph)
lang, script, dir, face := getLanguageAndScriptAndDirectionAndFont(rawText) shapedRunes, emojiMask := shapeText(rawText, FONT_SIZE)
shapedRunes := shapeText(shaping.Input{
Text: rawText,
RunStart: 0,
RunEnd: len(rawText),
Face: face,
Size: fixed.I(int(r.FontSize)),
Script: script,
Language: lang,
Direction: dir,
})
var wrapper shaping.LineWrapper var wrapper shaping.LineWrapper
it := shaping.NewSliceIterator([]shaping.Output{shapedRunes}) it := shaping.NewSliceIterator([]shaping.Output{shapedRunes})
@@ -204,7 +187,7 @@ func drawText(paragraphs []string, width, height int) image.Image {
for _, line := range lines { for _, line := range lines {
for _, out := range line { for _, out := range line {
r.DrawShapedRunAt(out, img, 0, FONT_SIZE*i*12/10) drawShapedRunAt(FONT_SIZE, color, out, emojiMask, img, 0, FONT_SIZE*i*12/10)
i++ i++
} }
} }

View File

@@ -5,7 +5,6 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"hash/maphash"
"html" "html"
"html/template" "html/template"
"math/rand" "math/rand"
@@ -13,7 +12,6 @@ import (
"regexp" "regexp"
"strings" "strings"
"time" "time"
"unsafe"
"github.com/microcosm-cc/bluemonday" "github.com/microcosm-cc/bluemonday"
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
@@ -529,7 +527,3 @@ func clamp(val, low, high int) int {
} }
return val return val
} }
func pointerHasher[V any](_ maphash.Seed, k *V) uint64 {
return uint64(uintptr(unsafe.Pointer(k)))
}