From bc2ef31a3bf0270744224a0033d53c422c2125ec Mon Sep 17 00:00:00 2001 From: Hamcha Date: Sun, 27 Mar 2022 22:40:10 +0200 Subject: [PATCH] Add photo modules --- go.mod | 2 +- go.sum | 4 +- main.go | 4 + modules/meme/mod.go | 233 ++++++++++++++++++++++++++++++++++++++++ modules/snapchat/mod.go | 206 +++++++++++++++++++++++++++++++++++ modules/unsplash/mod.go | 3 +- run.ps1 | 1 + run.sh | 1 + utils/text.go | 60 ++++++++++- 9 files changed, 508 insertions(+), 6 deletions(-) create mode 100644 modules/meme/mod.go create mode 100644 modules/snapchat/mod.go diff --git a/go.mod b/go.mod index ec313e7..218f526 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( git.fromouter.space/crunchy-rocks/draw2d v0.0.0-20190208101535-675e82cb6412 git.fromouter.space/crunchy-rocks/emoji v0.0.0-20181116142102-2188aadaf093 git.fromouter.space/crunchy-rocks/freetype v0.0.0-20181116104610-3115318f2577 - git.fromouter.space/hamcha/tg v0.1.0 + git.fromouter.space/hamcha/tg v0.2.1 git.sr.ht/~hamcha/containers v0.0.3 github.com/cockroachdb/pebble v0.0.0-20220323190648-7ad3551b2050 github.com/disintegration/imaging v1.6.2 diff --git a/go.sum b/go.sum index 59be4d5..b87b0ab 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,8 @@ git.fromouter.space/crunchy-rocks/emoji v0.0.0-20181116142102-2188aadaf093 h1:pa git.fromouter.space/crunchy-rocks/emoji v0.0.0-20181116142102-2188aadaf093/go.mod h1:bmXZYbLNSGivLP7yQlcQ605b1H1L8kzxAk8A7sAEWx0= git.fromouter.space/crunchy-rocks/freetype v0.0.0-20181116104610-3115318f2577 h1:rntW5kRu8s7dN8n7WWLHzAVZrnk7NS9YCdEQwA/O9pk= git.fromouter.space/crunchy-rocks/freetype v0.0.0-20181116104610-3115318f2577/go.mod h1:MlK0wT7XDuAuw0l/Z+Es2OHozCbBb2r9VxOy8bU58ow= -git.fromouter.space/hamcha/tg v0.1.0 h1:cJwL8pElkBtaDn7Bxa14zvlnBTTK8LdcCtcbBg7hEvk= -git.fromouter.space/hamcha/tg v0.1.0/go.mod h1:aIFj7n5FP+Zr/Zv6I6Kq4ZqhRxC12gXFQcC3iOakv9M= +git.fromouter.space/hamcha/tg v0.2.1 h1:cN14BhgbRLwYs4LkiKCKD9lDxwzbrpuRkQ6Buk4LdFw= +git.fromouter.space/hamcha/tg v0.2.1/go.mod h1:aIFj7n5FP+Zr/Zv6I6Kq4ZqhRxC12gXFQcC3iOakv9M= git.sr.ht/~hamcha/containers v0.0.3 h1:obG9X8s5iOIahVe+EGpkBDYmUAO78oTi9Y9gRurt334= git.sr.ht/~hamcha/containers v0.0.3/go.mod h1:RiZphUpy9t6EnL4Gf6uzByM9QrBoqRCEPo7kz2wzbhE= github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= diff --git a/main.go b/main.go index d5f19f9..7cfb8f9 100644 --- a/main.go +++ b/main.go @@ -9,9 +9,11 @@ import ( "git.fromouter.space/crunchy-rocks/clessy-ng/modules" "git.fromouter.space/crunchy-rocks/clessy-ng/modules/macro" + "git.fromouter.space/crunchy-rocks/clessy-ng/modules/meme" "git.fromouter.space/crunchy-rocks/clessy-ng/modules/metafora" "git.fromouter.space/crunchy-rocks/clessy-ng/modules/proverbio" "git.fromouter.space/crunchy-rocks/clessy-ng/modules/remind" + "git.fromouter.space/crunchy-rocks/clessy-ng/modules/snapchat" "git.fromouter.space/crunchy-rocks/clessy-ng/modules/unsplash" "git.fromouter.space/crunchy-rocks/clessy-ng/utils" @@ -26,6 +28,8 @@ var mods = map[string]modules.Module{ "macro": ¯o.Module{}, "remind": &remind.Module{}, "unsplash": &unsplash.Module{}, + "snapchat": &snapchat.Module{}, + "meme": &meme.Module{}, } func checkErr(err error, message string, args ...interface{}) { diff --git a/modules/meme/mod.go b/modules/meme/mod.go new file mode 100644 index 0000000..fb546a1 --- /dev/null +++ b/modules/meme/mod.go @@ -0,0 +1,233 @@ +package meme + +import ( + "bytes" + "image" + _ "image/gif" + "image/jpeg" + _ "image/png" + "log" + "math/rand" + "os" + "strings" + "time" + + "git.fromouter.space/crunchy-rocks/clessy-ng/modules" + "git.fromouter.space/crunchy-rocks/clessy-ng/utils" + + "git.fromouter.space/crunchy-rocks/draw2d" + "git.fromouter.space/crunchy-rocks/draw2d/draw2dimg" + "git.fromouter.space/crunchy-rocks/emoji" + "git.fromouter.space/crunchy-rocks/freetype" + "git.fromouter.space/hamcha/tg" +) + +var memeFontData draw2d.FontData + +type Module struct { + client *tg.Telegram + name string + emojis emoji.Table +} + +func (m *Module) Initialize(options modules.ModuleOptions) error { + m.client = options.API + m.name = options.Name + m.emojis = options.Emojis + + rand.Seed(time.Now().Unix()) + + fontfile := utils.RequireEnv("CLESSY_MEME_FONT") + bytes, err := os.ReadFile(fontfile) + if err != nil { + return err + } + + font, err := freetype.ParseFont(bytes) + if err != nil { + return err + } + + memeFontData = draw2d.FontData{ + Name: "impact", + Family: draw2d.FontFamilySans, + Style: 0, + } + draw2d.RegisterFont(memeFontData, font) + + log.Println("[meme] Loaded!") + + return nil +} + +func (m *Module) OnUpdate(update tg.APIUpdate) { + // Not a message? Ignore + if update.Message == nil { + return + } + message := *update.Message + + // Make replies work + if message.ReplyTo != nil && message.Text != nil && message.ReplyTo.Photo != nil { + message.Photo = message.ReplyTo.Photo + message.Caption = message.Text + } + + if message.Photo != nil && message.Caption != nil { + caption := *(message.Caption) + if strings.HasPrefix(caption, "/meme ") && len(caption) > 6 { + idx := strings.Index(caption, ";") + if idx < 0 { + m.client.SendTextMessage(tg.ClientTextMessageData{ + ChatID: message.Chat.ChatID, + Text: "Formato: /meme TESTO IN ALTO;TESTO IN BASSO", + ReplyID: &message.MessageID, + }) + return + } + + txtup := caption[6:idx] + txtdw := caption[idx+1:] + + maxsz := 0 + photo := tg.APIPhotoSize{} + for _, curphoto := range message.Photo { + if curphoto.Width > maxsz { + maxsz = curphoto.Width + photo = curphoto + } + } + + byt, err := m.client.GetFile(tg.FileRequestData{ + FileID: photo.FileID, + }) + if err != nil { + log.Printf("[memegen] Received error: %s\n", err.Error()) + m.client.SendTextMessage(tg.ClientTextMessageData{ + ChatID: message.Chat.ChatID, + Text: "ERRORE! @hamcha controlla la console!", + ReplyID: &message.MessageID, + }) + return + } + + img, _, err := image.Decode(bytes.NewReader(byt)) + if err != nil { + log.Printf("[memegen] Image decode error: %s\n", err.Error()) + m.client.SendTextMessage(tg.ClientTextMessageData{ + ChatID: message.Chat.ChatID, + Text: "ERRORE! Non riesco a leggere l'immagine", + ReplyID: &message.MessageID, + }) + return + } + + m.client.SendChatAction(tg.ClientChatActionData{ + ChatID: message.Chat.ChatID, + Action: tg.ActionUploadingPhoto, + }) + + //TODO Clean up this mess + + // Create target image + bounds := img.Bounds() + iwidth := float64(bounds.Size().X) + iheight := float64(bounds.Size().Y) + + timg := image.NewRGBA(bounds) + gc := draw2dimg.NewGraphicContext(timg) + gc.Emojis = m.emojis + gc.SetStrokeColor(image.Black) + gc.SetFillColor(image.White) + gc.SetFontData(memeFontData) + gc.DrawImage(img) + + write := func(text string, istop bool) { + text = strings.ToUpper(strings.TrimSpace(text)) + gc.Restore() + gc.Save() + + // Detect appropriate font size + scale := iheight / iwidth * (iwidth / 10) + gc.SetFontSize(scale) + gc.SetLineWidth(scale / 15) + + // Get NEW bounds + left, top, right, bottom := gc.GetStringBounds(text) + + width := right - left + texts := []string{text} + if width > iwidth { + // Split text + texts = utils.SplitCenter(text) + + // Get longest line + longer := float64(0) + longid := 0 + widths := make([]float64, len(texts)) + for id := range texts { + tleft, _, tright, _ := gc.GetStringBounds(texts[id]) + widths[id] = tright - tleft + if width > longer { + longer = widths[id] + longid = id + } + } + + // Still too big? Decrease font size again + iter := 0 + for width > iwidth && iter < 10 { + log.Println("Warning, resizing!") + gc.SetFontSize(scale * (0.8 - 0.1*float64(iter))) + left, top, right, bottom = gc.GetStringBounds(texts[longid]) + width = right - left + iter++ + } + } + + height := bottom - top + margin := float64(height / 50) + lines := float64(len(texts) - 1) + + gc.Save() + for id, txt := range texts { + gc.Save() + left, _, right, _ = gc.GetStringBounds(txt) + width = right - left + + y := float64(0) + if istop { + y = (height+margin)*float64(id+1) + margin*5 + } else { + y = iheight - (height * lines) + (height * float64(id)) - margin*5 + } + + gc.Translate((iwidth-width)/2, y) + gc.StrokeString(txt) + gc.FillString(txt) + gc.Restore() + } + } + write(txtup, true) + write(txtdw, false) + + buf := new(bytes.Buffer) + err = jpeg.Encode(buf, timg, &(jpeg.Options{Quality: 80})) + if err != nil { + log.Printf("[memegen] Image encode error: %s\n", err.Error()) + m.client.SendTextMessage(tg.ClientTextMessageData{ + ChatID: message.Chat.ChatID, + Text: "ERRORE! @hamcha controlla la console!", + ReplyID: &message.MessageID, + }) + return + } + m.client.SendPhoto(tg.ClientPhotoData{ + ChatID: message.Chat.ChatID, + Bytes: buf.Bytes(), + Filename: "meme.jpg", + ReplyID: &message.MessageID, + }) + } + } +} diff --git a/modules/snapchat/mod.go b/modules/snapchat/mod.go new file mode 100644 index 0000000..223d6da --- /dev/null +++ b/modules/snapchat/mod.go @@ -0,0 +1,206 @@ +package snapchat + +import ( + "bytes" + "image" + "image/color" + _ "image/gif" + "image/jpeg" + _ "image/png" + "log" + "math/rand" + "os" + "strings" + "time" + + "git.fromouter.space/crunchy-rocks/clessy-ng/modules" + "git.fromouter.space/crunchy-rocks/clessy-ng/utils" + + "git.fromouter.space/crunchy-rocks/draw2d" + "git.fromouter.space/crunchy-rocks/draw2d/draw2dimg" + "git.fromouter.space/crunchy-rocks/emoji" + "git.fromouter.space/crunchy-rocks/freetype" + "git.fromouter.space/hamcha/tg" +) + +var snapFontData draw2d.FontData + +type Module struct { + client *tg.Telegram + name string + emojis emoji.Table +} + +func (m *Module) Initialize(options modules.ModuleOptions) error { + m.client = options.API + m.name = options.Name + m.emojis = options.Emojis + + rand.Seed(time.Now().Unix()) + + fontfile := utils.RequireEnv("CLESSY_SNAPCHAT_FONT") + bytes, err := os.ReadFile(fontfile) + if err != nil { + return err + } + + font, err := freetype.ParseFont(bytes) + if err != nil { + return err + } + + snapFontData = draw2d.FontData{ + Name: "sourcesans", + Family: draw2d.FontFamilySans, + Style: draw2d.FontStyleBold, + } + draw2d.RegisterFont(snapFontData, font) + + log.Println("[snapchat] Loaded!") + + return nil +} + +func (m *Module) OnUpdate(update tg.APIUpdate) { + // Not a message? Ignore + if update.Message == nil { + return + } + message := *update.Message + + // Make replies work + if message.ReplyTo != nil && message.Text != nil && message.ReplyTo.Photo != nil { + message.Photo = message.ReplyTo.Photo + message.Caption = message.Text + } + + if message.Photo != nil && message.Caption != nil { + caption := *(message.Caption) + if strings.HasPrefix(caption, "/snap ") && len(caption) > 6 { + txt := strings.TrimSpace(caption[6:]) + + maxsz := 0 + photo := tg.APIPhotoSize{} + for _, curphoto := range message.Photo { + if curphoto.Width > maxsz { + maxsz = curphoto.Width + photo = curphoto + } + } + + byt, err := m.client.GetFile(tg.FileRequestData{ + FileID: photo.FileID, + }) + if err != nil { + log.Printf("[snapchat] Received error: %s\n", err.Error()) + m.client.SendTextMessage(tg.ClientTextMessageData{ + ChatID: message.Chat.ChatID, + Text: "ERRORE! @hamcha controlla la console!", + ReplyID: &message.MessageID, + }) + return + } + + img, _, err := image.Decode(bytes.NewReader(byt)) + if err != nil { + log.Printf("[snapchat] Image decode error: %s\n", err.Error()) + m.client.SendTextMessage(tg.ClientTextMessageData{ + ChatID: message.Chat.ChatID, + Text: "ERRORE! Non riesco a leggere l'immagine", + ReplyID: &message.MessageID, + }) + return + } + + m.client.SendChatAction(tg.ClientChatActionData{ + ChatID: message.Chat.ChatID, + Action: tg.ActionUploadingPhoto, + }) + + // Create target image + bounds := img.Bounds() + iwidth := float64(bounds.Size().Y) / 1.6 + iheight := float64(bounds.Size().Y) + + repos := iwidth < float64(bounds.Size().X) + if !repos { + iwidth = float64(bounds.Size().X) + } + + timg := image.NewRGBA(image.Rect(0, 0, int(iwidth), int(iheight))) + gc := draw2dimg.NewGraphicContext(timg) + gc.Emojis = m.emojis + gc.SetFontData(snapFontData) + + gc.Save() + if repos { + gc.Translate(-(float64(bounds.Size().X)-iwidth)/2, 0) + } + gc.DrawImage(img) + gc.Restore() + + scale := iwidth / 25 + gc.SetFontSize(scale) + + lineMargin := scale / 3 + boxMargin := lineMargin + topMargin := lineMargin / 6 + write := func(text string, startHeight float64) { + texts := utils.WordWrap(gc, strings.TrimSpace(text), iwidth*0.9) + totalHeight := startHeight + firstLine := 0. + for _, txt := range texts { + _, top, _, bottom := gc.GetStringBounds(txt) + height := (bottom - top) + if firstLine == 0 { + firstLine = height + } + totalHeight += lineMargin + height + } + + // Draw background + starty := startHeight - boxMargin - topMargin - firstLine + endy := totalHeight + boxMargin - firstLine + gc.Save() + gc.SetFillColor(color.RGBA{0, 0, 0, 160}) + gc.BeginPath() + gc.MoveTo(0, starty) + gc.LineTo(iwidth, starty) + gc.LineTo(iwidth, endy) + gc.LineTo(0, endy) + gc.Close() + gc.Fill() + gc.Restore() + + // Write lines + gc.SetFillColor(image.White) + height := startHeight + for _, txt := range texts { + left, top, right, bottom := gc.GetStringBounds(txt) + width := right - left + gc.FillStringAt(txt, (iwidth-width)/2, height) + height += lineMargin + (bottom - top) + } + } + write(txt, (rand.Float64()*0.4+0.3)*iheight) + + buf := new(bytes.Buffer) + err = jpeg.Encode(buf, timg, &(jpeg.Options{Quality: 80})) + if err != nil { + log.Printf("[snapchat] Image encode error: %s\n", err.Error()) + m.client.SendTextMessage(tg.ClientTextMessageData{ + ChatID: message.Chat.ChatID, + Text: "ERRORE! @hamcha controlla la console!", + ReplyID: &message.MessageID, + }) + return + } + m.client.SendPhoto(tg.ClientPhotoData{ + ChatID: message.Chat.ChatID, + Bytes: buf.Bytes(), + Filename: "meme.jpg", + ReplyID: &message.MessageID, + }) + } + } +} diff --git a/modules/unsplash/mod.go b/modules/unsplash/mod.go index 4be278c..9d64c81 100644 --- a/modules/unsplash/mod.go +++ b/modules/unsplash/mod.go @@ -2,7 +2,6 @@ package unsplash import ( "bytes" - "encoding/base64" "image" _ "image/gif" "image/jpeg" @@ -255,7 +254,7 @@ func (m *Module) OnUpdate(update tg.APIUpdate) { } m.client.SendPhoto(tg.ClientPhotoData{ ChatID: message.Chat.ChatID, - Bytes: base64.StdEncoding.EncodeToString(buf.Bytes()), + Bytes: buf.Bytes(), Filename: "quote.jpg", ReplyID: &message.MessageID, }) diff --git a/run.ps1 b/run.ps1 index 40dc47b..d5b0747 100644 --- a/run.ps1 +++ b/run.ps1 @@ -6,5 +6,6 @@ $env:CLESSY_EMOJI_PATH = "_data" $env:CLESSY_UNSPLASH_FONT = "_data/gill.ttf" $env:CLESSY_UNSPLASH_BG_PATH = "_data/pics" $env:CLESSY_MEME_FONT = "_data/impact.ttf" +$env:CLESSY_SNAPCHAT_FONT = "_data/source.ttf" mkdir -force _data/pics go run . \ No newline at end of file diff --git a/run.sh b/run.sh index f493b65..f61beac 100644 --- a/run.sh +++ b/run.sh @@ -5,5 +5,6 @@ export CLESSY_DB_DIR=_data/db export CLESSY_EMOJI_PATH=_data export CLESSY_UNSPLASH_FONT=_data/gill.ttf export CLESSY_MEME_FONT=_data/impact.ttf +export CLESSY_SNAPCHAT_FONT=_data/source.ttf mkdir -p _data/pics go run . \ No newline at end of file diff --git a/utils/text.go b/utils/text.go index b3f0af1..b44f94d 100644 --- a/utils/text.go +++ b/utils/text.go @@ -1,6 +1,11 @@ package utils -import "strings" +import ( + "strings" + "unicode" + + "git.fromouter.space/crunchy-rocks/draw2d" +) func abs(i int) int { if i < 0 { @@ -30,3 +35,56 @@ func SplitCenter(text string) []string { } return []string{strings.TrimSpace(text[:whitespaceBackIndex]), strings.TrimSpace(text[whitespaceBackIndex:])} } + +// Word wrapping code from https://github.com/fogleman/gg +// Copyright (C) 2016 Michael Fogleman +// Licensed under MIT (https://github.com/fogleman/gg/blob/master/LICENSE.md) + +func splitOnSpace(x string) []string { + var result []string + pi := 0 + ps := false + for i, c := range x { + s := unicode.IsSpace(c) + if s != ps && i > 0 { + result = append(result, x[pi:i]) + pi = i + } + ps = s + } + result = append(result, x[pi:]) + return result +} + +func WordWrap(gc draw2d.GraphicContext, s string, width float64) []string { + var result []string + for _, line := range strings.Split(s, "\n") { + fields := splitOnSpace(line) + if len(fields)%2 == 1 { + fields = append(fields, "") + } + x := "" + for i := 0; i < len(fields); i += 2 { + left, _, right, _ := gc.GetStringBounds(x + fields[i]) + w := right - left + if w > width { + if x == "" { + result = append(result, fields[i]) + x = "" + continue + } else { + result = append(result, x) + x = "" + } + } + x += fields[i] + fields[i+1] + } + if x != "" { + result = append(result, x) + } + } + for i, line := range result { + result[i] = strings.TrimSpace(line) + } + return result +}