diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bb23bbe --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +noto_emojis +example/emojis/out.png diff --git a/emoji.go b/emoji.go new file mode 100644 index 0000000..f6b36ed --- /dev/null +++ b/emoji.go @@ -0,0 +1,76 @@ +package freetype + +import ( + "errors" + "fmt" + "strings" + "unicode/utf8" +) + +type Emoji struct { + Codepoints []rune + Bytes int +} + +var ( + errNotAnEmoji = errors.New("not an emoji") +) + +func inrange(val, min, max rune) bool { + return val >= min && val < max +} + +// https://stackoverflow.com/questions/30757193/find-out-if-character-in-string-is-emoji/39425959 +func isEmoji(chr rune) bool { + switch { + case inrange(chr, 0x1f600, 0x1f64f), // Emoticons + inrange(chr, 0x1f300, 0x1f5ff), // Misc Symbols and Pictographs + inrange(chr, 0x1f680, 0x1f6ff), // Transport and Map + inrange(chr, 0x1f1e6, 0x1f1ff), // Regional country flags + inrange(chr, 0x2600, 0x26ff), // Misc symbols + inrange(chr, 0x2700, 0x27bf), // Dingbats + inrange(chr, 0xfe00, 0xfe0f), // Variation Selectors + inrange(chr, 0x1f900, 0x1f9ff), // Supplemental Symbols and Pictographs + inrange(chr, 127000, 127600), // Various asian characters + inrange(chr, 65024, 65039), // Variation selector + inrange(chr, 9100, 9300), // Misc items + inrange(chr, 8400, 8447): // Combining Diacritical Marks for Symbols + return true + } + return false +} + +func isZWJ(chr rune) bool { + return chr == 8205 +} + +func parseEmoji(str string) (Emoji, error) { + if len(str) < 1 { + return Emoji{}, errNotAnEmoji + } + + emoji := Emoji{} + + for _, r := range str { + // Check if rune is emoji + if !isEmoji(r) && !isZWJ(r) { + if len(emoji.Codepoints) < 1 { + return emoji, errNotAnEmoji + } + break + } + + emoji.Codepoints = append(emoji.Codepoints, r) + emoji.Bytes += utf8.RuneLen(r) + } + + return emoji, nil +} + +func (e Emoji) String() string { + codepoints := []string{} + for _, cp := range e.Codepoints { + codepoints = append(codepoints, fmt.Sprintf("%U", cp)) + } + return "Emoji(" + strings.Join(codepoints, ", ") + ")" +} diff --git a/example/emojis/main.go b/example/emojis/main.go new file mode 100644 index 0000000..7f859f5 --- /dev/null +++ b/example/emojis/main.go @@ -0,0 +1,124 @@ +// Copyright 2010 The Freetype-Go Authors. All rights reserved. +// Use of this source code is governed by your choice of either the +// FreeType License or the GNU General Public License version 2 (or +// any later version), both of which can be found in the LICENSE file. + +// +build example +// +// This build tag means that "go install github.com/golang/freetype/..." +// doesn't install this example program. Use "go run main.go" to run it or "go +// install -tags=example" to install it. + +package main + +import ( + "bufio" + "flag" + "fmt" + "image" + "image/color" + "image/draw" + "image/png" + "io/ioutil" + "log" + "os" + + "git.fromouter.space/crunchy-rocks/freetype" + "golang.org/x/image/font" +) + +var ( + dpi = flag.Float64("dpi", 72, "screen resolution in Dots Per Inch") + fontfile = flag.String("fontfile", "../../testdata/luxisr.ttf", "filename of the ttf font") + hinting = flag.String("hinting", "none", "none | full") + size = flag.Float64("size", 20, "font size in points") + spacing = flag.Float64("spacing", 1.5, "line spacing (e.g. 2 means double spaced)") + wonb = flag.Bool("whiteonblack", false, "white text on a black background") +) + +var text = []string{ + "Single glyph emoji: ๐Ÿ˜‚", + "Gender-modified emoji: ๐Ÿ’‚โ€โ™€๏ธ", + "Skin-modified emoji: ๐Ÿ–๐Ÿฝ", + "Mixed emoji: ๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง", + "๐Ÿ‘Œ๐Ÿ‘€๐Ÿ‘Œ๐Ÿ‘€๐Ÿ‘Œ๐Ÿ‘€๐Ÿ‘Œ๐Ÿ‘€๐Ÿ‘Œ๐Ÿ‘€ good shit goเฑฆิ sHit๐Ÿ‘Œ thats โœ” some good๐Ÿ‘Œ๐Ÿ‘Œshit right๐Ÿ‘Œ๐Ÿ‘Œ", + "th ๐Ÿ‘Œ ere๐Ÿ‘Œ๐Ÿ‘Œ๐Ÿ‘Œ rightโœ”there โœ”โœ”if i doโ€Šฦฝaาฏ soโ€‡my sel๏ฝ† ๐Ÿ’ฏ i say so ", + "๐Ÿ’ฏ thats what im talking about right there right there (chorus: สณแถฆแตสฐแต— แต—สฐแต‰สณแต‰)", + "mMMMMแŽทะœ๐Ÿ’ฏ ๐Ÿ‘Œ๐Ÿ‘Œ ๐Ÿ‘ŒะO0ะžเฌ ๏ผฏOO๏ผฏOะžเฌ เฌ Ooooแต’แต’แต’แต’แต’แต’แต’แต’แต’๐Ÿ‘Œ ๐Ÿ‘Œ๐Ÿ‘Œ ๐Ÿ‘Œ ๐Ÿ’ฏ ๐Ÿ‘Œ ๐Ÿ‘€ ๐Ÿ‘€ ๐Ÿ‘€ ๐Ÿ‘Œ๐Ÿ‘ŒGood shit", +} + +func main() { + flag.Parse() + + // Read the font data. + fontBytes, err := ioutil.ReadFile(*fontfile) + if err != nil { + log.Println(err) + return + } + f, err := freetype.ParseFont(fontBytes) + if err != nil { + log.Println(err) + return + } + + // Initialize the context. + fg, bg := image.Black, image.White + ruler := color.RGBA{0xdd, 0xdd, 0xdd, 0xff} + if *wonb { + fg, bg = image.White, image.Black + ruler = color.RGBA{0x22, 0x22, 0x22, 0xff} + } + rgba := image.NewRGBA(image.Rect(0, 0, 640, 480)) + draw.Draw(rgba, rgba.Bounds(), bg, image.ZP, draw.Src) + c := freetype.NewContext() + c.SetDPI(*dpi) + c.SetFont(f) + c.SetFontSize(*size) + c.SetClip(rgba.Bounds()) + c.SetDst(rgba) + c.SetSrc(fg) + switch *hinting { + default: + c.SetHinting(font.HintingNone) + case "full": + c.SetHinting(font.HintingFull) + } + + // Draw the guidelines. + for i := 0; i < 200; i++ { + rgba.Set(10, 10+i, ruler) + rgba.Set(10+i, 10, ruler) + } + + // Draw the text. + pt := freetype.Pt(10, 10+int(c.PointToFixed(*size)>>6)) + for _, s := range text { + _, err = c.DrawString(s, pt) + if err != nil { + log.Println(err) + return + } + pt.Y += c.PointToFixed(*size * *spacing) + } + + // Save that RGBA image to disk. + outFile, err := os.Create("out.png") + if err != nil { + log.Println(err) + os.Exit(1) + } + defer outFile.Close() + b := bufio.NewWriter(outFile) + err = png.Encode(b, rgba) + if err != nil { + log.Println(err) + os.Exit(1) + } + err = b.Flush() + if err != nil { + log.Println(err) + os.Exit(1) + } + fmt.Println("Wrote out.png OK.") +} diff --git a/freetype.go b/freetype.go index 888b16f..0b7ef6a 100644 --- a/freetype.go +++ b/freetype.go @@ -10,6 +10,7 @@ package freetype // import "git.fromouter.space/crunchy-rocks/freetype" import ( "errors" + "fmt" "image" "image/draw" @@ -232,8 +233,21 @@ func (c *Context) DrawString(s string, p fixed.Point26_6) (fixed.Point26_6, erro return fixed.Point26_6{}, errors.New("freetype: DrawText called with a nil font") } prev, hasPrev := truetype.Index(0), false - for _, rune := range s { - index := c.f.Index(rune) + nextchar := 0 + for index, r := range s { + // Check if we need to skip entries + if nextchar > index { + continue + } + // Check if rune is an emoji + if isEmoji(r) { + emoji, err := parseEmoji(s[index:]) + if err == nil { + fmt.Println("Found emoji:", emoji) + nextchar = index + emoji.Bytes + } + } + index := c.f.Index(r) if hasPrev { kern := c.f.Kern(c.scale, prev, index) if c.hinting != font.HintingNone {