naïve text highlighting.

This commit is contained in:
fiatjaf
2023-12-31 14:26:14 -03:00
parent ba42ca42e2
commit d85872a1b5
2 changed files with 96 additions and 13 deletions

View File

@@ -12,6 +12,7 @@ import (
"strings"
"sync"
"time"
"unicode"
"github.com/fogleman/gg"
"github.com/go-text/typesetting/di"
@@ -31,6 +32,16 @@ const (
scaleShift = 6
)
// highlighting stuff
type hlstate int
const (
hlNormal hlstate = 0
hlLink hlstate = 1
hlMention hlstate = 2
hlHashtag hlstate = 3
)
var (
supportedScripts = [nSupportedScripts]language.Script{
language.Unknown,
@@ -357,7 +368,7 @@ func shortenURLs(text string) string {
// but also, the most important change was to make it "shape" the same text, twice, with the default font and with
// the emoji font, then build an output of glyphs containing normal glyphs for when the referenced rune is not an
// emoji and an emoji glyph for when it is.
func shapeText(rawText []rune, fontSize int) (shaping.Output, []bool) {
func shapeText(rawText []rune, fontSize int) (shaping.Output, []bool, []hlstate) {
lang, script, dir, face := getLanguageAndScriptAndDirectionAndFont(rawText)
shaperLock.Lock()
@@ -449,9 +460,15 @@ func shapeText(rawText []rune, fontSize int) (shaping.Output, []bool) {
}
}
// this will be used to determine if we'll use a different color when rendering a glyph or not
hlMask := make([]hlstate, len(emojiBuffer.Info))
var hlState hlstate = hlNormal
// convert the shaped text into an output
glyphs := make([]shaping.Glyph, len(mainBuffer.Info))
for i := 0; i < len(glyphs); i++ {
// deciding if we'll render this as emoji or not
var buf *harfbuzz.Buffer
var font *harfbuzz.Font
if emojiMask[i] {
@@ -461,8 +478,57 @@ func shapeText(rawText []rune, fontSize int) (shaping.Output, []bool) {
buf = mainBuffer
font = mainFont
}
// current glyph specs
glyph := buf.Info[i]
// naïve text highlighting
switch hlState {
case hlNormal:
if glyph.Codepoint == '#' &&
len(buf.Info) > i+1 &&
unicode.In(buf.Info[i+1].Codepoint, unicode.Letter) {
hlState = hlHashtag
} else if glyph.Codepoint == 'h' &&
len(buf.Info) > i+1 &&
buf.Info[i+1].Codepoint == 't' &&
buf.Info[i+2].Codepoint == 't' &&
buf.Info[i+3].Codepoint == 'p' {
if buf.Info[i+4].Codepoint == 's' &&
buf.Info[i+5].Codepoint == ':' &&
buf.Info[i+6].Codepoint == '/' &&
buf.Info[i+7].Codepoint == '/' &&
buf.Info[i+8].Codepoint != ' ' {
hlState = hlLink
} else if buf.Info[i+4].Codepoint == ':' &&
buf.Info[i+5].Codepoint == '/' &&
buf.Info[i+6].Codepoint == '/' &&
buf.Info[i+7].Codepoint != ' ' {
hlState = hlLink
}
} else if glyph.Codepoint == '@' &&
len(buf.Info) > i+1 &&
unicode.In(buf.Info[i+1].Codepoint, unicode.Letter) {
hlState = hlMention
}
case hlLink:
if glyph.Codepoint == ' ' ||
glyph.Codepoint == ',' {
hlState = hlNormal
}
case hlMention:
if !unicode.In(glyph.Codepoint, unicode.Letter) {
hlState = hlNormal
}
case hlHashtag:
if !unicode.In(glyph.Codepoint, unicode.Letter) {
hlState = hlNormal
}
}
hlMask[i] = hlState
// ~
glyphs[i] = shaping.Glyph{
ClusterIndex: glyph.Cluster,
GlyphID: glyph.Glyph,
@@ -500,7 +566,7 @@ func shapeText(rawText []rune, fontSize int) (shaping.Output, []bool) {
}
out.RecalculateAll()
return out, emojiMask
return out, emojiMask, hlMask
}
// this function is copied from go-text/typesetting/shaping because shapeText needs it
@@ -546,12 +612,13 @@ func countClusters(glyphs []shaping.Glyph, textLen int, dir di.Progression) {
// this function is copied from go-text/render, but adapted to not require a "class" to be instantiated and also,
// more importantly, to take an emojiMask parameter, with the same length as out.Glyphs, to determine when a
// glyph should be rendered with the emoji font instead of with the default font
func drawShapedRunAt(
func drawShapedBlockAt(
img draw.Image,
fontSize int,
clr color.Color,
colors [4]color.Color,
out shaping.Output,
emojiMask []bool,
hlMask []hlstate,
maskBaseIndex int,
startX,
startY int,
@@ -559,11 +626,17 @@ func drawShapedRunAt(
scale := float32(fontSize) / float32(out.Face.Upem())
b := img.Bounds()
scanner := rasterx.NewScannerGV(b.Dx(), b.Dy(), img, b)
f := rasterx.NewFiller(b.Dx(), b.Dy(), scanner)
f.SetColor(clr)
var fillers [4]*rasterx.Filler
for i := range fillers {
scanner := rasterx.NewScannerGV(b.Dx(), b.Dy(), img, b)
fillers[i] = rasterx.NewFiller(b.Dx(), b.Dy(), scanner)
fillers[i].SetColor(colors[i])
}
x := float32(startX)
y := float32(startY)
for i, g := range out.Glyphs {
xPos := x + fixed266ToFloat(g.XOffset)
yPos := y - fixed266ToFloat(g.YOffset)
@@ -575,6 +648,8 @@ func drawShapedRunAt(
currentScale = float32(fontSize) / float32(face.Upem())
}
f := fillers[hlMask[maskBaseIndex+i]]
data := face.GlyphData(g.GlyphID)
switch format := data.(type) {
case api.GlyphOutline:
@@ -588,7 +663,10 @@ func drawShapedRunAt(
charsWritten++
x += fixed266ToFloat(g.XAdvance)
}
f.Draw()
for _, filler := range fillers {
filler.Draw()
}
return charsWritten, int(math.Ceil(float64(x)))
}

View File

@@ -179,10 +179,9 @@ func drawImage(paragraphs []string, style Style, metadata sdk.ProfileMetadata, d
func drawText(paragraphs []string, width, height int, dynamicResize bool) image.Image {
FONT_SIZE := 25
color := color.RGBA{R: 255, G: 230, B: 238, A: 255}
img := image.NewNRGBA(image.Rect(0, 0, width, height))
joinedContent := strings.Join(paragraphs, " \n") // The space before the \n is necessary
joinedContent := strings.Join(paragraphs, " \n") // the space before the \n is necessary
if dynamicResize && len(joinedContent) < 141 {
fontSizeTest := 7.0
step := 0.5
@@ -209,7 +208,7 @@ func drawText(paragraphs []string, width, height int, dynamicResize bool) image.
for _, paragraph := range paragraphs {
rawText := []rune(paragraph)
shapedRunes, emojiMask := shapeText(rawText, FONT_SIZE)
shapedRunes, emojiMask, hlMask := shapeText(rawText, FONT_SIZE)
var wrapper shaping.LineWrapper
it := shaping.NewSliceIterator([]shaping.Output{shapedRunes})
@@ -218,12 +217,18 @@ func drawText(paragraphs []string, width, height int, dynamicResize bool) image.
totalCharsWritten := 0
for _, line := range lines {
for _, out := range line {
charsWritten, _ := drawShapedRunAt(
charsWritten, _ := drawShapedBlockAt(
img,
FONT_SIZE,
color,
[4]color.Color{
color.RGBA{R: 255, G: 230, B: 238, A: 255}, // normal
color.RGBA{R: 146, G: 193, B: 198, A: 255}, // links
color.RGBA{R: 84, G: 211, B: 168, A: 255}, // mentions
color.RGBA{R: 156, G: 186, B: 53, A: 255}, // hashtags
},
out,
emojiMask,
hlMask,
totalCharsWritten,
0,
FONT_SIZE*lineNumber*12/10,