diff --git a/image_utils.go b/image_utils.go index 52d9922..8b39736 100644 --- a/image_utils.go +++ b/image_utils.go @@ -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))) } diff --git a/render_image.go b/render_image.go index e88b3f2..1f340fd 100644 --- a/render_image.go +++ b/render_image.go @@ -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,