From 86f6a09ea3e4efcfcde927ce4a8279c04b610c1b Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Tue, 26 Dec 2023 11:02:43 -0300 Subject: [PATCH] using a custom RunIterator that takes emojis into account and shapes them with a different font. it doesn't actually work since the different outputs returned are treated by LineWrapper as necessarily belonging to different lines, so we'll have to do something different. --- go.mod | 8 ++--- go.sum | 12 +++++--- image_utils.go | 81 +++++++++++++++++++++++++++++++++++++++++++++++++ render_image.go | 31 +++++++++---------- 4 files changed, 107 insertions(+), 25 deletions(-) diff --git a/go.mod b/go.mod index fb6c53d..7248968 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,13 @@ go 1.21.4 require ( github.com/PuerkitoBio/goquery v1.5.0 - github.com/apatters/go-wordwrap v1.0.0 github.com/dgraph-io/badger/v4 v4.2.0 + github.com/dmolesUC3/emoji v0.0.0-20190226181050-1849526eb21f github.com/fiatjaf/eventstore v0.3.3 github.com/fiatjaf/khatru v0.2.1 github.com/fogleman/gg v1.3.0 + github.com/go-text/render v0.0.0-20230619120952-35bccb6164b8 + github.com/go-text/typesetting v0.0.0-20231221124458-48cc05a56658 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 github.com/gomarkdown/markdown v0.0.0-20231115200524-a660076da3fd github.com/kelseyhightower/envconfig v1.4.0 @@ -17,6 +19,7 @@ require ( github.com/nbd-wtf/nostr-sdk v0.0.4 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 github.com/pelletier/go-toml v1.9.5 + github.com/pemistahl/lingua-go v1.4.0 github.com/rs/cors v1.10.0 github.com/rs/zerolog v1.29.1 github.com/stretchr/testify v1.8.4 @@ -41,8 +44,6 @@ require ( github.com/dgraph-io/ristretto v0.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/fasthttp/websocket v1.5.3 // indirect - github.com/go-text/render v0.0.0-20230619120952-35bccb6164b8 // indirect - github.com/go-text/typesetting v0.0.0-20231221124458-48cc05a56658 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/ws v1.3.1 // indirect @@ -58,7 +59,6 @@ require ( github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.14 // indirect - github.com/pemistahl/lingua-go v1.4.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/puzpuzpuz/xsync/v2 v2.5.1 // indirect diff --git a/go.sum b/go.sum index ed841ff..e33d5fd 100644 --- a/go.sum +++ b/go.sum @@ -7,8 +7,6 @@ github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/ github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o= github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= -github.com/apatters/go-wordwrap v1.0.0 h1:G6ni4Pt7/I4ED+A5ZvsK8e9XwETD04veDKxEL2QN830= -github.com/apatters/go-wordwrap v1.0.0/go.mod h1:3sM7HcArQ+utXnjDQ4d1xjrd8b/wbKuDr/RmbiHgjwI= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= @@ -59,6 +57,8 @@ github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWa github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dmolesUC3/emoji v0.0.0-20190226181050-1849526eb21f h1:l4W9/rRyCN5QtPsgMvERrQ3mKSsaO/b191TPxe2ROO8= +github.com/dmolesUC3/emoji v0.0.0-20190226181050-1849526eb21f/go.mod h1:+5FBTP/tHiFB2yrIvWaLq9PchgdvVu99lBK5ZrEzl9w= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= @@ -78,10 +78,10 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/go-text/render v0.0.0-20230619120952-35bccb6164b8 h1:VkKnvzbvHqgEfm351rfr8Uclu5fnwq8HP2ximUzJsBM= github.com/go-text/render v0.0.0-20230619120952-35bccb6164b8/go.mod h1:h29xCucjNsDcYb7+0rJokxVwYAq+9kQ19WiFuBKkYtc= -github.com/go-text/typesetting v0.0.0-20230616162802-9c17dd34aa4a h1:VjN8ttdfklC0dnAdKbZqGNESdERUxtE3l8a/4Grgarc= -github.com/go-text/typesetting v0.0.0-20230616162802-9c17dd34aa4a/go.mod h1:evDBbvNR/KaVFZ2ZlDSOWWXIUKq0wCOEtzLxRM8SG3k= github.com/go-text/typesetting v0.0.0-20231221124458-48cc05a56658 h1:KeDKnC99J3l5qJK4zV13Au2UwPn4N20TnIlM0YvILj8= github.com/go-text/typesetting v0.0.0-20231221124458-48cc05a56658/go.mod h1:d22AnmeKq/on0HNv73UFriMKc4Ez6EqZAofLhAzpSzI= +github.com/go-text/typesetting-utils v0.0.0-20231211103740-d9332ae51f04 h1:zBx+p/W2aQYtNuyZNcTfinWvXBQwYtDfme051PR/lAY= +github.com/go-text/typesetting-utils v0.0.0-20231211103740-d9332ae51f04/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= @@ -146,6 +146,8 @@ github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= @@ -200,7 +202,6 @@ github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+g github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -323,6 +324,7 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= diff --git a/image_utils.go b/image_utils.go index 26f457d..5f55017 100644 --- a/image_utils.go +++ b/image_utils.go @@ -12,11 +12,14 @@ import ( "strings" "time" + "github.com/dmolesUC3/emoji" "github.com/fogleman/gg" "github.com/go-text/typesetting/di" "github.com/go-text/typesetting/font" "github.com/go-text/typesetting/language" + "github.com/go-text/typesetting/shaping" "github.com/pemistahl/lingua-go" + "golang.org/x/image/math/fixed" ) const nSupportedScripts = 13 @@ -325,3 +328,81 @@ func shortenURLs(text string) string { return strings.Replace(urlStr, "/////", strings.Join(pathParts, "/"), 1) }) } + +type shapedOutputIterator struct { + rawText []rune + idx int + savedIdx int + shaper *shaping.HarfbuzzShaper + fontSize int + face font.Face + language language.Language + script language.Script + direction di.Direction +} + +var _ shaping.RunIterator = (*shapedOutputIterator)(nil) + +func (it *shapedOutputIterator) Next() (int, shaping.Output, bool) { + idx, nextIdx, run, ok := it.readNext() + if ok { + it.idx = nextIdx + } + return idx, run, ok +} + +func (it *shapedOutputIterator) Peek() (int, shaping.Output, bool) { + idx, _, out, more := it.readNext() + return idx, out, more +} + +func (it *shapedOutputIterator) readNext() (int, int, shaping.Output, bool) { + if it.idx >= len(it.rawText) { + return it.idx, -1, shaping.Output{}, false + } + + // if the next character is an emoji then return a block of emojis + if emoji.IsEmoji(it.rawText[it.idx]) { + shapedEmoji := it.shaper.Shape(shaping.Input{ + Text: it.rawText, + RunStart: it.idx, + RunEnd: it.idx + 1, + Face: emojiFont, + Size: fixed.I(int(it.fontSize)), + Script: it.script, + Language: it.language, + Direction: it.direction, + }) + return it.idx, it.idx + 1, shapedEmoji, true + } + // otherwise we consume runes until we find an emoji and return everything + + var runesConsumed int = 0 + for r, rn := range it.rawText[it.idx:] { + if emoji.IsEmoji(rn) { + // reached an emoji, stop now + break + } + runesConsumed = r + } + + shapedRunes := it.shaper.Shape(shaping.Input{ + Text: it.rawText, + RunStart: it.idx, + RunEnd: it.idx + runesConsumed + 1, + Face: it.face, + Size: fixed.I(int(it.fontSize)), + Script: it.script, + Language: it.language, + Direction: it.direction, + }) + return it.idx, it.idx + runesConsumed + 1, shapedRunes, true +} + +func (it *shapedOutputIterator) Save() { + it.savedIdx = it.idx +} + +func (it *shapedOutputIterator) Restore() { + it.idx = it.savedIdx +} diff --git a/render_image.go b/render_image.go index 6db6229..8cb941b 100644 --- a/render_image.go +++ b/render_image.go @@ -19,7 +19,6 @@ import ( sdk "github.com/nbd-wtf/nostr-sdk" "github.com/nfnt/resize" xfont "golang.org/x/image/font" - "golang.org/x/image/math/fixed" ) const ( @@ -183,24 +182,24 @@ func drawText(paragraphs []string, width, height int) image.Image { i := 1 for _, paragraph := range paragraphs { - text := []rune(paragraph) + rawText := []rune(paragraph) + if len(rawText) == 0 { + rawText = []rune{' '} + } - lang, script, dir, face := getLanguageAndScriptAndDirectionAndFont(text) - shaper := &shaping.HarfbuzzShaper{} - - shapedRunes := shaper.Shape(shaping.Input{ - Text: text, - RunStart: 0, - RunEnd: len(text), - Face: face, - Size: fixed.I(int(r.FontSize)), - Script: script, - Language: lang, - Direction: dir, - }) + lang, script, dir, face := getLanguageAndScriptAndDirectionAndFont(rawText) + iterator := &shapedOutputIterator{ + rawText: rawText, + shaper: &shaping.HarfbuzzShaper{}, + fontSize: FONT_SIZE, + language: lang, + script: script, + direction: dir, + face: face, + } var wrapper shaping.LineWrapper - lines, _ := wrapper.WrapParagraph(shaping.WrapConfig{}, width, []rune(text), shaping.NewSliceIterator([]shaping.Output{shapedRunes})) + lines, _ := wrapper.WrapParagraph(shaping.WrapConfig{}, width, rawText, iterator) for _, line := range lines { for _, out := range line {