diff --git a/emoji.go b/emoji.go index f6b36ed..4ff62d9 100644 --- a/emoji.go +++ b/emoji.go @@ -1,76 +1,138 @@ package freetype import ( - "errors" "fmt" + "os" + "path/filepath" + "strconv" "strings" - "unicode/utf8" ) -type Emoji struct { - Codepoints []rune - Bytes int +type emoji struct { + Codepoint []rune + IsEmoji bool + Path string + Sub emojiTable } -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 +func (e emoji) String() (str string) { + str = "Emoji(" + for _, cprune := range e.Codepoint { + str += fmt.Sprintf("%U ", cprune) } - return false -} - -func isZWJ(chr rune) bool { - return chr == 8205 -} - -func parseEmoji(str string) (Emoji, error) { - if len(str) < 1 { - return Emoji{}, errNotAnEmoji + str = strings.TrimRight(str, " ") + ")" + if e.Sub != nil { + str += "+" } + if e.IsEmoji { + str += fmt.Sprintf("\n └ Path: %s", e.Path) + } + return str + "\n" +} - emoji := Emoji{} +type emojiTable map[rune]emoji - for _, r := range str { - // Check if rune is emoji - if !isEmoji(r) && !isZWJ(r) { - if len(emoji.Codepoints) < 1 { - return emoji, errNotAnEmoji - } +func (em emojiTable) Find(str string) (emj emoji, length int) { + length = 0 + for i, r := range str { + e, ok := em[r] + if !ok { break } - - emoji.Codepoints = append(emoji.Codepoints, r) - emoji.Bytes += utf8.RuneLen(r) + // Check if there are more bytes to check + if len(str) > i && e.Sub != nil { + newemj, len := e.Sub.Find(str[i:]) + if len > 0 { + length += len + emj = newemj + return + } + } + if e.IsEmoji { + emj = e + } + length++ } - - return emoji, nil + return } -func (e Emoji) String() string { - codepoints := []string{} - for _, cp := range e.Codepoints { - codepoints = append(codepoints, fmt.Sprintf("%U", cp)) - } - return "Emoji(" + strings.Join(codepoints, ", ") + ")" +func (em emojiTable) IsEmoji(cp rune) bool { + _, ok := em[cp] + return ok +} + +func (em emojiTable) tostring(indent string, nomarker bool) (str string) { + counter := len(em) + marker := "│ " + if nomarker { + marker = " " + } + for r, emo := range em { + listr := "├" + if counter == 1 { + listr = "└" + } + str += fmt.Sprintf(indent+"%s%s %U", marker, listr, r) + if emo.IsEmoji { + str += fmt.Sprintf(": %s", emo.Path) + } + str += "\n" + if emo.Sub != nil { + str += emo.Sub.tostring(indent+marker, counter == 1) + } + counter-- + } + return +} + +func (em emojiTable) String() string { + return "Emoji table\n" + em.tostring("", true) +} + +func scanEmojiDirectory(emojipath string) (tab emojiTable, err error) { + tab = make(emojiTable) + filepath.Walk(emojipath, func(path string, info os.FileInfo, err error) error { + // Ignore non-images + if !strings.HasSuffix(strings.ToLower(path), ".png") { + return nil + } + + // Get icon filename + emojiname := filepath.Base(path) + + // Strip prefix and suffix + extsep := strings.LastIndexByte(emojiname, '.') + basesep := strings.IndexByte(emojiname, '_') + codepointstr := emojiname[basesep+2 : extsep] + + // Split codepoints by separator (_) + codepointlist := strings.Split(codepointstr, "_") + + // Parse codepoints to runes + curtab := &tab + codepoints := []rune{} + for cpi, cpstr := range codepointlist { + num, err := strconv.ParseInt(cpstr, 16, 32) + if err != nil { + return fmt.Errorf("malformed icon filename: %s (codepoints are not valid int32)", emojiname) + } + cprune := rune(num) + newemo := (*curtab)[cprune] + codepoints = append(codepoints, cprune) + newemo.Codepoint = codepoints[:] + if len(codepointlist) < cpi+2 { + // Set as emoji + newemo.IsEmoji = true + newemo.Path = path + } else { + // Add sub-entry + newemo.Sub = make(emojiTable) + } + (*curtab)[cprune] = newemo + curtab = &newemo.Sub + } + fmt.Println(codepointstr) + return nil + }) + return tab, err } diff --git a/example/emojis/main.go b/example/emojis/main.go index 7f859f5..ff948dd 100644 --- a/example/emojis/main.go +++ b/example/emojis/main.go @@ -34,6 +34,7 @@ var ( 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") + emojidir = flag.String("emojidir", "../../noto_emojis", "Path to emojis") ) var text = []string{ @@ -72,6 +73,7 @@ func main() { rgba := image.NewRGBA(image.Rect(0, 0, 640, 480)) draw.Draw(rgba, rgba.Bounds(), bg, image.ZP, draw.Src) c := freetype.NewContext() + c.ScanEmojis(*emojidir) c.SetDPI(*dpi) c.SetFont(f) c.SetFontSize(*size) diff --git a/freetype.go b/freetype.go index 0b7ef6a..3c2b157 100644 --- a/freetype.go +++ b/freetype.go @@ -74,6 +74,8 @@ type Context struct { hinting font.Hinting // cache is the glyph cache. cache [nGlyphs * nXFractions * nYFractions]cacheEntry + // emojis is the table of all parsable emojis + emojis emojiTable } // PointToFixed converts the given number of points (as in "a 12 point font") @@ -240,12 +242,10 @@ func (c *Context) DrawString(s string, p fixed.Point26_6) (fixed.Point26_6, erro 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 - } + if c.emojis != nil && c.emojis.IsEmoji(r) { + icon, length := c.emojis.Find(s[index:]) + fmt.Printf("Found emoji at position #%d: (len %d)\n %s", index, length, icon.String()) + nextchar = index + length } index := c.f.Index(r) if hasPrev { @@ -353,3 +353,9 @@ func NewContext() *Context { scale: 12 << 6, } } + +// ScanEmojis scans a directory for emojis and adds them to the context's emoji repertoire +func (c *Context) ScanEmojis(path string) (err error) { + c.emojis, err = scanEmojiDirectory(path) + return +}