Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Additional metadata, Text Slot Rotation and Support for OTF fonts #58

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ module github.com/mattermost/mattermost-plugin-memes
go 1.12

require (
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
github.com/fogleman/gg v1.3.0
github.com/gorilla/mux v1.7.4
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/mattermost/mattermost-server/v5 v5.24.0-rc1
github.com/mholt/archiver/v3 v3.3.0
github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.6.1
golang.org/x/image v0.0.0-20200430140353-33d19683fad8
golang.org/x/image v0.0.0-20220302094943-723b81ca9867
golang.org/x/text v0.3.7 // indirect
gopkg.in/yaml.v2 v2.3.0
)
10 changes: 7 additions & 3 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
Expand Down Expand Up @@ -534,8 +536,8 @@ golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86h
golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200430140353-33d19683fad8 h1:6WW6V3x1P/jokJBpRQYUJnMHRP6isStQwCozxnU7XQw=
golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20220302094943-723b81ca9867 h1:TcHcE0vrmgzNH1v3ppjcMGbhG5+9fMuvOmUYwNEF4q4=
golang.org/x/image v0.0.0-20220302094943-723b81ca9867/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
Expand Down Expand Up @@ -615,8 +617,10 @@ golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9 h1:YTzHMGlqJu67/uEo1lBv0n3wB
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
Expand Down
11 changes: 5 additions & 6 deletions server/meme/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ package meme

import (
"image"
"image/draw"

"github.com/fogleman/gg"
)

type Template struct {
Expand All @@ -12,16 +13,14 @@ type Template struct {
}

func (t *Template) Render(text []string) (image.Image, error) {
b := t.Image.Bounds()
img := image.NewRGBA(image.Rect(0, 0, b.Dx(), b.Dy()))
draw.Draw(img, img.Bounds(), t.Image, b.Min, draw.Src)
dc := gg.NewContextForImage(t.Image)

for i, slot := range t.TextSlots {
if i >= len(text) {
break
}
slot.Render(img, text[i])
slot.Render(dc, text[i], DEBUG)
}

return img, nil
return dc.Image(), nil
}
244 changes: 100 additions & 144 deletions server/meme/text_slot.go
Original file line number Diff line number Diff line change
@@ -1,198 +1,154 @@
package meme

import (
"fmt"
"image"
"image/color"
"image/draw"
"strings"
"unicode"

"github.com/golang/freetype/truetype"
"github.com/fogleman/gg"
"golang.org/x/image/font"
"golang.org/x/image/font/opentype"
"golang.org/x/image/math/fixed"
)

type HorizontalAlignment int

const (
Left HorizontalAlignment = -1
Center HorizontalAlignment = 0
Right HorizontalAlignment = 1
Left HorizontalAlignment = iota
Center
Right
)

type VerticalAlignment int

const (
Top VerticalAlignment = -1
Middle VerticalAlignment = 0
Bottom VerticalAlignment = 1
Top VerticalAlignment = iota
Middle
Bottom
)

const DEBUG = false
const lineSpacing = 0.8

type TextSlot struct {
Bounds image.Rectangle
Font *truetype.Font
MaxFontSize float64
HorizontalAlignment HorizontalAlignment
VerticalAlignment VerticalAlignment
Font *opentype.Font
TextColor color.Color
OutlineColor color.Color
HorizontalAlignment HorizontalAlignment
VerticalAlignment VerticalAlignment
OutlineWidth int
AllUppercase bool
Bounds image.Rectangle
MaxFontSize float64
Rotation float64
}

func (s *TextSlot) Render(img draw.Image, text string) {
func (s *TextSlot) Render(dc *gg.Context, text string, debug bool) {
if s.AllUppercase {
text = strings.ToUpper(text)
}
dc.Push()
// Compute font size by taking measurement of a text
face, _, textHeight := faceForSlot(text, s.Font, s.MaxFontSize, s.Bounds.Dx(), s.Bounds.Dy())
dc.SetFontFace(face)

rotCenterX := float64((s.Bounds.Min.X + s.Bounds.Dx()) / 2)
rotCenterY := float64((s.Bounds.Min.Y + s.Bounds.Dy()) / 2)

dc.RotateAbout(gg.Radians(s.Rotation),
rotCenterX, rotCenterY)
dc.SetColor(s.OutlineColor)

layout := s.TextLayout(text)
if layout == nil {
return
outlineWidth := s.OutlineWidth // "stroke" size
if outlineWidth == 0 {
outlineWidth = 8
}

textColor := s.TextColor
if textColor == nil {
textColor = color.Black
}

for i, line := range layout.Lines {
if s.OutlineColor != nil {
// it's okay, memes aren't supposed to look good
offset := layout.Face.Metrics().Height / 16
for _, delta := range []fixed.Point26_6{
{X: offset, Y: offset},
{X: -offset, Y: offset},
{X: -offset, Y: -offset},
{X: offset, Y: -offset},
} {
drawer := font.Drawer{
Dst: img,
Src: image.NewUniform(s.OutlineColor),
Face: layout.Face,
Dot: layout.LinePositions[i].Add(delta),
}
drawer.DrawString(line)
}
}
xStart := float64(s.Bounds.Min.X)
xAlign := gg.Align(s.HorizontalAlignment)

// Bottom padding for the text, because the string measurements account for top-padding but not bottom
yAlign := 0.1

yStart := float64(s.Bounds.Min.Y)
switch s.VerticalAlignment {
case Top:
yStart = float64(s.Bounds.Min.Y)
case Middle:
yStart = (float64(s.Bounds.Dy())-textHeight)/2.0 +
float64(s.Bounds.Min.Y)
case Bottom:
yStart = float64(s.Bounds.Max.Y) - textHeight
default:
break
}

drawer := font.Drawer{
Dst: img,
Src: image.NewUniform(textColor),
Face: layout.Face,
Dot: layout.LinePositions[i],
if s.OutlineColor != nil {
offset := face.Metrics().Height / 256 * fixed.Int26_6(outlineWidth)
for _, delta := range []fixed.Point26_6{
{X: offset, Y: offset},
{X: -offset, Y: offset},
{X: -offset, Y: -offset},
{X: offset, Y: -offset},
} {
x := xStart + float64(delta.X)/64
y := yStart + float64(delta.Y)/64
dc.DrawStringWrapped(text, x, y, 0, yAlign, float64(s.Bounds.Dx()), lineSpacing, xAlign)
}
drawer.DrawString(line)
}
}

type TextLayout struct {
Face font.Face
Lines []string
LinePositions []fixed.Point26_6
dc.SetColor(textColor)
dc.DrawStringWrapped(text,
xStart,
yStart,
0, yAlign, float64(s.Bounds.Dx()), lineSpacing, xAlign)

if debug {
fmt.Printf("X %v Y %v W %v H %v\n", float64(s.Bounds.Min.X), float64(s.Bounds.Min.Y),
float64(s.Bounds.Dx()), float64(s.Bounds.Dy()))
dc.SetRGB(1, 0, 0)
dc.DrawRectangle(float64(s.Bounds.Min.X), float64(s.Bounds.Min.Y),
float64(s.Bounds.Dx()), float64(s.Bounds.Dy()))
dc.Stroke()
}

dc.Pop()
}

func (s *TextSlot) TextLayout(text string) *TextLayout {
fontSize := s.MaxFontSize
func faceForSlot(text string, fontt *opentype.Font, maxFontSize float64, width int, height int) (font.Face, float64, float64) {
fontSize := maxFontSize
if fontSize == 0.0 {
fontSize = 80.0
fontSize = 80
}

hlimit := fixed.Int26_6(s.Bounds.Dx() * 64)
vlimit := fixed.Int26_6(s.Bounds.Dy() * 64)

w := 0.0
h := 0.0
dc := gg.NewContext(10, 10)
face, _ := opentype.NewFace(fontt, &opentype.FaceOptions{
Size: fontSize,
DPI: 72,
Hinting: font.HintingNone,
})
for fontSize >= 6.0 {
face := truetype.NewFace(s.Font, &truetype.Options{
Size: fontSize,
face, _ = opentype.NewFace(fontt, &opentype.FaceOptions{
Size: fontSize,
DPI: 72,
Hinting: font.HintingNone,
})
lineLimit := s.Bounds.Dy() / int(face.Metrics().Height/64)
lines, widths := lines(face, text, hlimit)
if len(lines) > lineLimit {
dc.SetFontFace(face)
lines := dc.WordWrap(text, float64(width))
w, h = dc.MeasureMultilineString(strings.Join(lines, "\n"), lineSpacing)
if w > float64(width) || h > float64(height)*lineSpacing {
fontSize -= (fontSize + 9) / 10
continue
}

layout := &TextLayout{
Face: face,
Lines: lines,
LinePositions: make([]fixed.Point26_6, len(lines)),
}

y := fixed.Int26_6(s.Bounds.Min.Y * 64)
totalHeight := face.Metrics().Height.Mul(fixed.Int26_6(len(lines) * 64))
switch s.VerticalAlignment {
case Middle:
y += (vlimit - totalHeight) / 2
case Bottom:
y += (vlimit - totalHeight)
}

for i, width := range widths {
x := fixed.Int26_6(s.Bounds.Min.X * 64)
switch s.HorizontalAlignment {
case Center:
x += (hlimit - width) / 2
case Right:
x += (hlimit - width)
}
y += face.Metrics().Height
layout.LinePositions[i] = fixed.Point26_6{
X: x,
Y: y,
}
}
return layout
}

return nil
}

func lines(face font.Face, text string, limit fixed.Int26_6) (lines []string, widths []fixed.Int26_6) {
for text != "" {
line, width, remaining := firstLine(face, text, limit)
if line == "" {
return nil, nil
}
lines = append(lines, line)
widths = append(widths, width)
text = remaining
}
return
}

func firstLine(face font.Face, text string, limit fixed.Int26_6) (string, fixed.Int26_6, string) {
text = strings.TrimSpace(text)

pos := 0
lastBreak := 0

var width fixed.Int26_6
var lastBreakWidth fixed.Int26_6

var prev rune = -1
for _, r := range text {
advance, ok := face.GlyphAdvance(r)
if !ok {
continue
}
if prev >= 0 {
advance += face.Kern(prev, r)
}

if unicode.IsSpace(r) && !unicode.IsSpace(prev) {
lastBreak = pos
lastBreakWidth = width
}

if width+advance > limit {
if lastBreak == 0 {
return string([]rune(text)[:pos]), width, string([]rune(text)[pos:])
}
return string([]rune(text)[:lastBreak]), lastBreakWidth, string([]rune(text)[lastBreak:])
}

pos++
width += advance
prev = r
break
}

return text, width, ""
return face, w, h
}
Loading