mirror of
https://github.com/aljazceru/njump.git
synced 2025-12-17 14:24:27 +01:00
successfully mixing in a normal font + the emoji font for some runes only through a myriad of weird hacks.
This commit is contained in:
173
image_utils.go
173
image_utils.go
@@ -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))
|
||||||
|
}
|
||||||
|
|||||||
@@ -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++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
6
utils.go
6
utils.go
@@ -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)))
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user