From 2f556b44d1f3aa3f1755d40750bf2e05886b6217 Mon Sep 17 00:00:00 2001 From: Robin Krom Date: Mon, 24 Oct 2022 08:09:55 +0200 Subject: [PATCH] Trying to make a bit more sense of the code, this is one of the first steps... didn't optimize anything yet. --- .../Controls/Emoji/ChunkHelper.cs | 69 +++++ .../Controls/Emoji/EmojiControl.cs | 54 ++++ .../Controls/Emoji/EmojiData.cs | 199 +++++++++++++ .../Controls/{ => Emoji}/EmojiPicker.xaml | 12 +- .../Controls/{ => Emoji}/EmojiPicker.xaml.cs | 1 + src/Greenshot.Editor/Controls/Emoji/Emojis.cs | 70 +++++ .../Emoji}/emoji-test.txt | 0 src/Greenshot.Editor/Controls/EmojiControl.cs | 33 --- src/Greenshot.Editor/Controls/EmojiData.cs | 262 ------------------ .../Drawing/{ => Emoji}/EmojiContainer.cs | 4 +- .../Drawing/Emoji/EmojiRenderer.cs | 120 ++++++++ .../Emoji}/TwemojiMozilla.ttf | Bin src/Greenshot.Editor/Drawing/EmojiRenderer.cs | 98 ------- src/Greenshot.Editor/Drawing/Surface.cs | 1 + .../Forms/ImageEditorForm.Designer.cs | 6 +- src/Greenshot.Editor/Forms/ImageEditorForm.cs | 2 +- src/Greenshot.Editor/Greenshot.Editor.csproj | 10 +- 17 files changed, 533 insertions(+), 408 deletions(-) create mode 100644 src/Greenshot.Editor/Controls/Emoji/ChunkHelper.cs create mode 100644 src/Greenshot.Editor/Controls/Emoji/EmojiControl.cs create mode 100644 src/Greenshot.Editor/Controls/Emoji/EmojiData.cs rename src/Greenshot.Editor/Controls/{ => Emoji}/EmojiPicker.xaml (93%) rename src/Greenshot.Editor/Controls/{ => Emoji}/EmojiPicker.xaml.cs (99%) create mode 100644 src/Greenshot.Editor/Controls/Emoji/Emojis.cs rename src/Greenshot.Editor/{Resources => Controls/Emoji}/emoji-test.txt (100%) delete mode 100644 src/Greenshot.Editor/Controls/EmojiControl.cs delete mode 100644 src/Greenshot.Editor/Controls/EmojiData.cs rename src/Greenshot.Editor/Drawing/{ => Emoji}/EmojiContainer.cs (97%) create mode 100644 src/Greenshot.Editor/Drawing/Emoji/EmojiRenderer.cs rename src/Greenshot.Editor/{Resources => Drawing/Emoji}/TwemojiMozilla.ttf (100%) delete mode 100644 src/Greenshot.Editor/Drawing/EmojiRenderer.cs diff --git a/src/Greenshot.Editor/Controls/Emoji/ChunkHelper.cs b/src/Greenshot.Editor/Controls/Emoji/ChunkHelper.cs new file mode 100644 index 000000000..d4aa24002 --- /dev/null +++ b/src/Greenshot.Editor/Controls/Emoji/ChunkHelper.cs @@ -0,0 +1,69 @@ +/* + * Greenshot - a free and open source screenshot tool + * Copyright (C) 2007-2021 Thomas Braun, Jens Klingen, Robin Krom + * + * For more information see: https://getgreenshot.org/ + * The Greenshot project is hosted on GitHub https://github.com/greenshot/greenshot + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 1 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace Greenshot.Editor.Controls.Emoji; + +/// +/// This seems to enumerate multiple IEnumerables. +/// TODO: Replace this with LINQ +/// +/// +internal sealed class ChunkHelper : IEnumerable> +{ + private readonly IEnumerable _elements; + private readonly int _size; + private bool _hasMore; + + public ChunkHelper(IEnumerable elements, int size) + { + _elements = elements; + _size = size; + } + + public IEnumerator> GetEnumerator() + { + using var enumerator = _elements.GetEnumerator(); + _hasMore = enumerator.MoveNext(); + while (_hasMore) + { + yield return GetNextBatch(enumerator).ToList(); + } + } + + private IEnumerable GetNextBatch(IEnumerator enumerator) + { + for (int i = 0; i < _size; ++i) + { + yield return enumerator.Current; + _hasMore = enumerator.MoveNext(); + if (!_hasMore) + { + yield break; + } + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} \ No newline at end of file diff --git a/src/Greenshot.Editor/Controls/Emoji/EmojiControl.cs b/src/Greenshot.Editor/Controls/Emoji/EmojiControl.cs new file mode 100644 index 000000000..0589c5ee1 --- /dev/null +++ b/src/Greenshot.Editor/Controls/Emoji/EmojiControl.cs @@ -0,0 +1,54 @@ +/* + * Greenshot - a free and open source screenshot tool + * Copyright (C) 2007-2021 Thomas Braun, Jens Klingen, Robin Krom, Francis Noel + * + * For more information see: https://getgreenshot.org/ + * The Greenshot project is hosted on GitHub https://github.com/greenshot/greenshot + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 1 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using Greenshot.Editor.Drawing.Emoji; + +namespace Greenshot.Editor.Controls.Emoji +{ + internal class EmojiControl : Image + { + public static readonly DependencyProperty EmojiProperty = DependencyProperty.Register("Emoji", typeof(string), typeof(EmojiControl), new PropertyMetadata(default(string), OnEmojiPropertyChanged)); + + private static void OnEmojiPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + ((EmojiControl)d).Source = null; + } + + public string Emoji + { + get => (string)GetValue(EmojiProperty); + set => SetValue(EmojiProperty, value); + } + + protected override void OnRender(DrawingContext dc) + { + if (Source == null && !string.IsNullOrEmpty(Emoji)) + { + Source = EmojiRenderer.GetBitmapSource(Emoji, iconSize: 48); + } + + base.OnRender(dc); + } + } +} diff --git a/src/Greenshot.Editor/Controls/Emoji/EmojiData.cs b/src/Greenshot.Editor/Controls/Emoji/EmojiData.cs new file mode 100644 index 000000000..b053ab3f4 --- /dev/null +++ b/src/Greenshot.Editor/Controls/Emoji/EmojiData.cs @@ -0,0 +1,199 @@ +// +// Emoji.Wpf — Emoji support for WPF +// +// Copyright © 2017—2021 Sam Hocevar +// +// This library is free software. It comes without any warranty, to +// the extent permitted by applicable law. You can redistribute it +// and/or modify it under the terms of the Do What the Fuck You Want +// to Public License, Version 2, as published by the WTFPL Task Force. +// See http://www.wtfpl.net/ for more details. +// + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; +using System.Xml; +using System.Xml.Serialization; +using SixLabors.Fonts.Unicode; + +namespace Greenshot.Editor.Controls.Emoji +{ + /// + /// This class processes the emoji-test.txt to generate a list of possible emoji depending ony skin tone and hairstyle. + /// + public static class EmojiData + { + private const string FilePath = "emojis.xml"; + + public static Emojis Data { get; private set; } = new Emojis(); + + public static void Load() + { + var x = new XmlSerializer(typeof(Emojis)); + + if (File.Exists(FilePath)) + { + Data = (Emojis)x.Deserialize(new XmlTextReader(FilePath)); + } + else + { + // To be removed + ParseEmojiList(); + x.Serialize(new XmlTextWriter(FilePath, Encoding.UTF8), Data); + } + } + + private static readonly List SkinToneComponents = new List + { + "🏻", // light skin tone + "🏼", // medium-light skin tone + "🏽", // medium skin tone + "🏾", // medium-dark skin tone + "🏿", // dark skin tone + }; + + private static readonly List HairStyleComponents = new List + { + "🦰", // red hair + "🦱", // curly hair + "🦳", // white hair + "🦲", // bald + }; + + private static string ToColonSyntax(string s) + => Regex.Replace(s.Trim().ToLowerInvariant(), "[^a-z0-9]+", "-"); + + private static void ParseEmojiList() + { + var lookupByName = new Dictionary(); + var matchGroup = new Regex(@"^# group: (.*)"); + var matchSubgroup = new Regex(@"^# subgroup: (.*)"); + var matchSequence = new Regex(@"^([0-9a-fA-F ]+[0-9a-fA-F]).*; *([-a-z]*) *# [^ ]* (E[0-9.]* )?(.*)"); + var matchSkinTone = new Regex($"({string.Join("|", SkinToneComponents)})"); + var matchHairStyle = new Regex($"({string.Join("|", HairStyleComponents)})"); + + var adult = "(👨|👩)(🏻|🏼|🏽|🏾|🏿)?"; + var child = "(👦|👧|👶)(🏻|🏼|🏽|🏾|🏿)?"; + var matchFamily = new Regex($"{adult}(\u200d{adult})*(\u200d{child})+"); + + var qualifiedLut = new Dictionary(); + var allText = new List(); + + Emojis.Group currentGroup = null; + Emojis.Group currentSubgroup = null; + + foreach (var line in EmojiDescriptionLines()) + { + var m = matchGroup.Match(line); + if (m.Success) + { + currentGroup = new Emojis.Group { Name = m.Groups[1].ToString() }; + Data.Groups.Add(currentGroup); + continue; + } + + m = matchSubgroup.Match(line); + if (m.Success) + { + currentSubgroup = new Emojis.Group { Name = m.Groups[1].ToString() }; + currentGroup.SubGroups.Add(currentSubgroup); + continue; + } + + m = matchSequence.Match(line); + if (m.Success) + { + string sequence = m.Groups[1].ToString(); + string name = m.Groups[4].ToString(); + + string text = string.Join("", from n in sequence.Split(' ') + select char.ConvertFromUtf32(Convert.ToInt32(n, 16))); + bool has_modifier = false; + + if (matchFamily.Match(text).Success) + { + // If this is a family emoji, no need to add it to our big matching + // regex, since the match_family regex is already included. + } + else + { + // Construct a regex to replace e.g. "🏻" with "(🏻|🏼|🏽|🏾|🏿)" in a big + // regex so that we can match all variations of this Emoji even if they are + // not in the standard. + bool hasNonfirstModifier = false; + var regexText = matchSkinTone.Replace( + matchHairStyle.Replace(text, (x) => + { + has_modifier = true; + hasNonfirstModifier |= x.Value != HairStyleComponents[0]; + return matchHairStyle.ToString(); + }), (x) => + { + has_modifier = true; + hasNonfirstModifier |= x.Value != SkinToneComponents[0]; + return matchSkinTone.ToString(); + }); + + if (!hasNonfirstModifier) + { + allText.Add(has_modifier ? regexText : text); + } + } + + // If there is already a differently-qualified version of this character, skip it. + // FIXME: this only works well if fully-qualified appears first in the list. + var unqualified = text.Replace("\ufe0f", ""); + if (qualifiedLut.ContainsKey(unqualified)) + { + continue; + } + + // Fix simple fully-qualified emojis + if (CodePoint.GetCodePointCount(text.AsSpan()) == 2) + { + text = text.TrimEnd('\ufe0f'); + } + + qualifiedLut[unqualified] = text; + + var emoji = new Emojis.Emoji { Name = name, Text = text, }; + + lookupByName[ToColonSyntax(name)] = emoji; + + // Get the left part of the name and check whether we’re a variation of an existing + // emoji. If so, append to that emoji. Otherwise, add to current subgroup. + // FIXME: does not work properly because variations can appear before the generic emoji + if (name.Contains(":") && lookupByName.TryGetValue(ToColonSyntax(name.Split(':')[0]), out var parent_emoji)) + { + parent_emoji.Variations.Add(emoji); + } + else + { + currentSubgroup.Emojis.Add(emoji); + } + } + } + + // Remove the Component group. Not sure we want to have the skin tones in the picker. + Data.Groups.RemoveAll(g => g.Name == "Component"); + } + + private static IEnumerable EmojiDescriptionLines() + { + var exeDirectory = Path.GetDirectoryName(Assembly.GetCallingAssembly().Location); + var emojiTestFile = Path.Combine(exeDirectory, @"emoji-test.txt"); + if (!File.Exists(emojiTestFile)) + { + throw new FileNotFoundException($"Can't find {emojiTestFile}, bad installation?"); + } + using var fileStream = new FileStream(emojiTestFile, FileMode.Open, FileAccess.Read); + using var streamReader = new StreamReader(fileStream); + return streamReader.ReadToEnd().Split('\r', '\n'); + } + } +} \ No newline at end of file diff --git a/src/Greenshot.Editor/Controls/EmojiPicker.xaml b/src/Greenshot.Editor/Controls/Emoji/EmojiPicker.xaml similarity index 93% rename from src/Greenshot.Editor/Controls/EmojiPicker.xaml rename to src/Greenshot.Editor/Controls/Emoji/EmojiPicker.xaml index 011754560..ddbb78b2d 100644 --- a/src/Greenshot.Editor/Controls/EmojiPicker.xaml +++ b/src/Greenshot.Editor/Controls/Emoji/EmojiPicker.xaml @@ -4,7 +4,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:controls="clr-namespace:Greenshot.Editor.Controls" + xmlns:emoji="clr-namespace:Greenshot.Editor.Controls.Emoji" Orientation="Horizontal" mc:Ignorable="d"> @@ -36,7 +36,7 @@ @@ -48,7 +48,7 @@ x:Name="VariationButton" Background="Transparent" BorderBrush="Transparent" Click="OnEmojiPicked" Focusable="False" ToolTip="{Binding Path=Name}"> - - + - + - + diff --git a/src/Greenshot.Editor/Controls/EmojiPicker.xaml.cs b/src/Greenshot.Editor/Controls/Emoji/EmojiPicker.xaml.cs similarity index 99% rename from src/Greenshot.Editor/Controls/EmojiPicker.xaml.cs rename to src/Greenshot.Editor/Controls/Emoji/EmojiPicker.xaml.cs index 4e36b6339..35f346cd6 100644 --- a/src/Greenshot.Editor/Controls/EmojiPicker.xaml.cs +++ b/src/Greenshot.Editor/Controls/Emoji/EmojiPicker.xaml.cs @@ -17,6 +17,7 @@ using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Input; +using Greenshot.Editor.Controls.Emoji; namespace Greenshot.Editor.Controls { diff --git a/src/Greenshot.Editor/Controls/Emoji/Emojis.cs b/src/Greenshot.Editor/Controls/Emoji/Emojis.cs new file mode 100644 index 000000000..8ddf41e34 --- /dev/null +++ b/src/Greenshot.Editor/Controls/Emoji/Emojis.cs @@ -0,0 +1,70 @@ +/* + * Greenshot - a free and open source screenshot tool + * Copyright (C) 2007-2021 Thomas Braun, Jens Klingen, Robin Krom + * + * For more information see: https://getgreenshot.org/ + * The Greenshot project is hosted on GitHub https://github.com/greenshot/greenshot + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 1 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Serialization; + +namespace Greenshot.Editor.Controls.Emoji; + +public class Emojis +{ + [XmlElement(ElementName = "Group")] + public List Groups { get; set; } = new(); + + public class Group + { + [XmlAttribute] + public string Name { get; set; } + + [XmlElement(ElementName = "Group")] + public List SubGroups { get; set; } = new(); + + [XmlElement(ElementName = "Emoji")] + public List Emojis { get; set; } = new(); + + public IEnumerable> EmojiChunkList => new ChunkHelper(EmojiList, 8); + + public string Icon => SubGroups.FirstOrDefault()?.Emojis.FirstOrDefault()?.Text; + + public IEnumerable EmojiList + => from s in SubGroups + from e in s.Emojis + select e; + } + + public class Emoji + { + [XmlAttribute] + public string Name { get; set; } + + [XmlAttribute] + public string Text { get; set; } + + [XmlArray] + public List Variations { get; set; } = new(); + + public bool HasVariations => Variations.Count > 0; + + public IEnumerable AllVariations => HasVariations ? new[] { this }.Union(Variations) : Array.Empty(); + } +} \ No newline at end of file diff --git a/src/Greenshot.Editor/Resources/emoji-test.txt b/src/Greenshot.Editor/Controls/Emoji/emoji-test.txt similarity index 100% rename from src/Greenshot.Editor/Resources/emoji-test.txt rename to src/Greenshot.Editor/Controls/Emoji/emoji-test.txt diff --git a/src/Greenshot.Editor/Controls/EmojiControl.cs b/src/Greenshot.Editor/Controls/EmojiControl.cs deleted file mode 100644 index c3fc09b6e..000000000 --- a/src/Greenshot.Editor/Controls/EmojiControl.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Windows; -using System.Windows.Controls; -using System.Windows.Media; -using Greenshot.Editor.Drawing; - -namespace Greenshot.Editor.Controls -{ - internal class EmojiControl : Image - { - public static readonly DependencyProperty EmojiProperty = DependencyProperty.Register("Emoji", typeof(string), typeof(EmojiControl), new PropertyMetadata(default(string), OnEmojiPropertyChanged)); - - private static void OnEmojiPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - ((EmojiControl)d).Source = null; - } - - public string Emoji - { - get { return (string)GetValue(EmojiProperty); } - set { SetValue(EmojiProperty, value); } - } - - protected override void OnRender(DrawingContext dc) - { - if (Source == null && !string.IsNullOrEmpty(Emoji)) - { - Source = EmojiRenderer.GetBitmapSource(Emoji, iconSize: 48); - } - - base.OnRender(dc); - } - } -} diff --git a/src/Greenshot.Editor/Controls/EmojiData.cs b/src/Greenshot.Editor/Controls/EmojiData.cs deleted file mode 100644 index 0e1eb0039..000000000 --- a/src/Greenshot.Editor/Controls/EmojiData.cs +++ /dev/null @@ -1,262 +0,0 @@ -// -// Emoji.Wpf — Emoji support for WPF -// -// Copyright © 2017—2021 Sam Hocevar -// -// This library is free software. It comes without any warranty, to -// the extent permitted by applicable law. You can redistribute it -// and/or modify it under the terms of the Do What the Fuck You Want -// to Public License, Version 2, as published by the WTFPL Task Force. -// See http://www.wtfpl.net/ for more details. -// - -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Text.RegularExpressions; -using System.Xml; -using System.Xml.Serialization; -using SixLabors.Fonts.Unicode; - -namespace Greenshot.Editor.Controls -{ - public static class EmojiData - { - private const string FilePath = "emojis.xml"; - - public static Emojis Data { get; private set; } = new Emojis(); - - public static void Load() - { - var x = new XmlSerializer(typeof(Emojis)); - - if (File.Exists(FilePath)) - { - Data = (Emojis)x.Deserialize(new XmlTextReader(FilePath)); - } - else - { - // To be removed - ParseEmojiList(); - x.Serialize(new XmlTextWriter(FilePath, Encoding.UTF8), Data); - } - } - - private static List SkinToneComponents = new List - { - "🏻", // light skin tone - "🏼", // medium-light skin tone - "🏽", // medium skin tone - "🏾", // medium-dark skin tone - "🏿", // dark skin tone - }; - - private static List HairStyleComponents = new List - { - "🦰", // red hair - "🦱", // curly hair - "🦳", // white hair - "🦲", // bald - }; - - private static string ToColonSyntax(string s) - => Regex.Replace(s.Trim().ToLowerInvariant(), "[^a-z0-9]+", "-"); - - private static void ParseEmojiList() - { - var lookup_by_name = new Dictionary(); - var match_group = new Regex(@"^# group: (.*)"); - var match_subgroup = new Regex(@"^# subgroup: (.*)"); - var match_sequence = new Regex(@"^([0-9a-fA-F ]+[0-9a-fA-F]).*; *([-a-z]*) *# [^ ]* (E[0-9.]* )?(.*)"); - var match_skin_tone = new Regex($"({string.Join("|", SkinToneComponents)})"); - var match_hair_style = new Regex($"({string.Join("|", HairStyleComponents)})"); - - var adult = "(👨|👩)(🏻|🏼|🏽|🏾|🏿)?"; - var child = "(👦|👧|👶)(🏻|🏼|🏽|🏾|🏿)?"; - var match_family = new Regex($"{adult}(\u200d{adult})*(\u200d{child})+"); - - var qualified_lut = new Dictionary(); - var alltext = new List(); - - Emojis.Group current_group = null; - Emojis.Group current_subgroup = null; - - foreach (var line in EmojiDescriptionLines()) - { - var m = match_group.Match(line); - if (m.Success) - { - current_group = new Emojis.Group { Name = m.Groups[1].ToString() }; - Data.Groups.Add(current_group); - continue; - } - - m = match_subgroup.Match(line); - if (m.Success) - { - current_subgroup = new Emojis.Group { Name = m.Groups[1].ToString() }; - current_group.SubGroups.Add(current_subgroup); - continue; - } - - m = match_sequence.Match(line); - if (m.Success) - { - string sequence = m.Groups[1].ToString(); - string name = m.Groups[4].ToString(); - - string text = string.Join("", from n in sequence.Split(' ') - select char.ConvertFromUtf32(Convert.ToInt32(n, 16))); - bool has_modifier = false; - - if (match_family.Match(text).Success) - { - // If this is a family emoji, no need to add it to our big matching - // regex, since the match_family regex is already included. - } - else - { - // Construct a regex to replace e.g. "🏻" with "(🏻|🏼|🏽|🏾|🏿)" in a big - // regex so that we can match all variations of this Emoji even if they are - // not in the standard. - bool has_nonfirst_modifier = false; - var regex_text = match_skin_tone.Replace( - match_hair_style.Replace(text, (x) => - { - has_modifier = true; - has_nonfirst_modifier |= x.Value != HairStyleComponents[0]; - return match_hair_style.ToString(); - }), (x) => - { - has_modifier = true; - has_nonfirst_modifier |= x.Value != SkinToneComponents[0]; - return match_skin_tone.ToString(); - }); - - if (!has_nonfirst_modifier) - alltext.Add(has_modifier ? regex_text : text); - } - - // If there is already a differently-qualified version of this character, skip it. - // FIXME: this only works well if fully-qualified appears first in the list. - var unqualified = text.Replace("\ufe0f", ""); - if (qualified_lut.ContainsKey(unqualified)) - continue; - - // Fix simple fully-qualified emojis - if (CodePoint.GetCodePointCount(text.AsSpan()) == 2) - { - text = text.TrimEnd('\ufe0f'); - } - - qualified_lut[unqualified] = text; - - var emoji = new Emojis.Emoji { Name = name, Text = text, }; - - lookup_by_name[ToColonSyntax(name)] = emoji; - - // Get the left part of the name and check whether we’re a variation of an existing - // emoji. If so, append to that emoji. Otherwise, add to current subgroup. - // FIXME: does not work properly because variations can appear before the generic emoji - if (name.Contains(":") && lookup_by_name.TryGetValue(ToColonSyntax(name.Split(':')[0]), out var parent_emoji)) - { - parent_emoji.Variations.Add(emoji); - } - else - current_subgroup.Emojis.Add(emoji); - } - } - - // Remove the Component group. Not sure we want to have the skin tones in the picker. - Data.Groups.RemoveAll(g => g.Name == "Component"); - } - - private static IEnumerable EmojiDescriptionLines() - { - using var fileStream = new FileStream(Path.Combine(Path.GetDirectoryName(Assembly.GetCallingAssembly().Location), @"emoji-test.txt"), FileMode.Open, FileAccess.Read); - using var streamReader = new StreamReader(fileStream); - return streamReader.ReadToEnd().Split('\r', '\n'); - } - } - - public class Emojis - { - [XmlElement(ElementName = "Group")] - public List Groups { get; set; } = new(); - - public class Group - { - [XmlAttribute] - public string Name { get; set; } - - [XmlElement(ElementName = "Group")] - public List SubGroups { get; set; } = new(); - - [XmlElement(ElementName = "Emoji")] - public List Emojis { get; set; } = new(); - - public IEnumerable> EmojiChunkList => new ChunkHelper(EmojiList, 8); - - public string Icon => SubGroups.FirstOrDefault()?.Emojis.FirstOrDefault()?.Text; - - public IEnumerable EmojiList - => from s in SubGroups - from e in s.Emojis - select e; - } - - public class Emoji - { - [XmlAttribute] - public string Name { get; set; } - - [XmlAttribute] - public string Text { get; set; } - - [XmlArray] - public List Variations { get; set; } - - public bool HasVariations => Variations.Count > 0; - - public IEnumerable AllVariations => HasVariations ? new[] { this }.Union(Variations) : Array.Empty(); - } - } - - sealed class ChunkHelper : IEnumerable> - { - public ChunkHelper(IEnumerable elements, int size) - { - m_elements = elements; - m_size = size; - } - - public IEnumerator> GetEnumerator() - { - using var enumerator = m_elements.GetEnumerator(); - m_has_more = enumerator.MoveNext(); - while (m_has_more) - yield return GetNextBatch(enumerator).ToList(); - } - - private IEnumerable GetNextBatch(IEnumerator enumerator) - { - for (int i = 0; i < m_size; ++i) - { - yield return enumerator.Current; - m_has_more = enumerator.MoveNext(); - if (!m_has_more) - yield break; - } - } - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - private readonly IEnumerable m_elements; - private readonly int m_size; - private bool m_has_more; - } -} \ No newline at end of file diff --git a/src/Greenshot.Editor/Drawing/EmojiContainer.cs b/src/Greenshot.Editor/Drawing/Emoji/EmojiContainer.cs similarity index 97% rename from src/Greenshot.Editor/Drawing/EmojiContainer.cs rename to src/Greenshot.Editor/Drawing/Emoji/EmojiContainer.cs index 17b11cc8e..0e9eb0497 100644 --- a/src/Greenshot.Editor/Drawing/EmojiContainer.cs +++ b/src/Greenshot.Editor/Drawing/Emoji/EmojiContainer.cs @@ -30,13 +30,13 @@ using Greenshot.Editor.Controls; using Greenshot.Editor.Helpers; using Image = System.Drawing.Image; -namespace Greenshot.Editor.Drawing +namespace Greenshot.Editor.Drawing.Emoji { /// /// Description of EmojiContainer. /// [Serializable] - public class EmojiContainer : VectorGraphicsContainer, IEmojiContainer, IHaveScaleOptions + public sealed class EmojiContainer : VectorGraphicsContainer, IEmojiContainer, IHaveScaleOptions { [NonSerialized] private static EmojiContainer _currentContainer; [NonSerialized] private static ElementHost _emojiPickerHost; diff --git a/src/Greenshot.Editor/Drawing/Emoji/EmojiRenderer.cs b/src/Greenshot.Editor/Drawing/Emoji/EmojiRenderer.cs new file mode 100644 index 000000000..c79442510 --- /dev/null +++ b/src/Greenshot.Editor/Drawing/Emoji/EmojiRenderer.cs @@ -0,0 +1,120 @@ +/* + * Greenshot - a free and open source screenshot tool + * Copyright (C) 2007-2021 Thomas Braun, Jens Klingen, Robin Krom, Francis Noel + * + * For more information see: https://getgreenshot.org/ + * The Greenshot project is hosted on GitHub https://github.com/greenshot/greenshot + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 1 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +using System; +using System.IO; +using System.Reflection; +using SixLabors.Fonts; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace Greenshot.Editor.Drawing.Emoji +{ + /// + /// This will render Emoji + /// + internal static class EmojiRenderer + { + private static readonly FontCollection _fontCollection = new FontCollection(); + + private static readonly Lazy _twemojiFontFamily = new Lazy(() => + { + var exeDirectory = Path.GetDirectoryName(Assembly.GetCallingAssembly().Location); + var twemojiFontFile = Path.Combine(exeDirectory, "TwemojiMozilla.ttf"); + if (!File.Exists(twemojiFontFile)) + { + throw new FileNotFoundException($"Can't find {twemojiFontFile}, bad installation?"); + } + + using var fileStream = new FileStream(twemojiFontFile, FileMode.Open, FileAccess.Read); + _fontCollection.Add(fileStream); + _fontCollection.TryGet("Twemoji Mozilla", out var fontFamily); + return fontFamily; + }); + + public static Image GetImage(string emoji, int iconSize) + { + var image = new Image(iconSize, iconSize); + + RenderEmoji(emoji, iconSize, image); + return image; + } + + private static void RenderEmoji(string emoji, int iconSize, Image image) + { + var fontFamily = _twemojiFontFamily.Value; + var font = fontFamily.CreateFont(iconSize, FontStyle.Regular); + var verticalOffset = font.Size * 0.045f; + var textOptions = new TextOptions(font) + { + Origin = new PointF(font.Size / 2.0f, font.Size / 2.0f + verticalOffset), + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + }; + + image.Mutate(x => x.DrawText(textOptions, emoji, Color.Black)); + } + + public static System.Drawing.Image GetBitmap(string emoji, int iconSize) + { + int width = iconSize; + int height = iconSize; + + var bitmap = new System.Drawing.Bitmap(width, height, System.Drawing.Imaging.PixelFormat.Format32bppArgb); + var bitmapData = bitmap.LockBits(new System.Drawing.Rectangle(0, 0, width, height), System.Drawing.Imaging.ImageLockMode.ReadWrite, System.Drawing.Imaging.PixelFormat.Format32bppArgb); + + try + { + unsafe + { + var image = Image.WrapMemory((void*)bitmapData.Scan0, width: width, height: height); + RenderEmoji(emoji, iconSize, image); + } + } + finally + { + bitmap.UnlockBits(bitmapData); + } + + return bitmap; + } + + public static System.Windows.Media.Imaging.BitmapSource GetBitmapSource(string emoji, int iconSize) + { + var pixelFormat = System.Windows.Media.PixelFormats.Bgra32; + int width = iconSize; + int height = iconSize; + int stride = (width * pixelFormat.BitsPerPixel + 7) / 8; + byte[] pixels = new byte[stride * height]; + + var image = Image.WrapMemory(byteMemory: pixels, width: width, height: height); + + RenderEmoji(emoji, iconSize, image); + + var source = System.Windows.Media.Imaging.BitmapSource.Create(pixelWidth: width, pixelHeight: height, dpiX: 96, dpiY: 96, pixelFormat: pixelFormat, palette: null, pixels: pixels, stride: stride); + source.Freeze(); + + return source; + } + } +} diff --git a/src/Greenshot.Editor/Resources/TwemojiMozilla.ttf b/src/Greenshot.Editor/Drawing/Emoji/TwemojiMozilla.ttf similarity index 100% rename from src/Greenshot.Editor/Resources/TwemojiMozilla.ttf rename to src/Greenshot.Editor/Drawing/Emoji/TwemojiMozilla.ttf diff --git a/src/Greenshot.Editor/Drawing/EmojiRenderer.cs b/src/Greenshot.Editor/Drawing/EmojiRenderer.cs deleted file mode 100644 index 37d827cb4..000000000 --- a/src/Greenshot.Editor/Drawing/EmojiRenderer.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using System.Drawing; -using System.Drawing.Imaging; -using System.IO; -using System.Reflection; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using SixLabors.Fonts; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; -using FontFamily = SixLabors.Fonts.FontFamily; -using FontStyle = SixLabors.Fonts.FontStyle; -using Image = System.Drawing.Image; -using PixelFormat = System.Drawing.Imaging.PixelFormat; -using TextOptions = SixLabors.Fonts.TextOptions; - -namespace Greenshot.Editor.Drawing -{ - internal static class EmojiRenderer - { - static FontCollection _fontCollection = new FontCollection(); - - private static Lazy _twemoji = new Lazy(() => - { - using var fileStream = new FileStream(Path.Combine(Path.GetDirectoryName(Assembly.GetCallingAssembly().Location), @"TwemojiMozilla.ttf"), FileMode.Open, FileAccess.Read); - _fontCollection.Add(fileStream); - _fontCollection.TryGet("Twemoji Mozilla", out var fontFamily); - return fontFamily; - }); - - public static Image GetImage(string emoji, int iconSize) - { - var image = new Image(iconSize, iconSize); - - RenderEmoji(emoji, iconSize, image); - return image; - } - - private static void RenderEmoji(string emoji, int iconSize, SixLabors.ImageSharp.Image image) - { - var fontFamily = _twemoji.Value; - var font = fontFamily.CreateFont(iconSize, FontStyle.Regular); - var verticalOffset = font.Size * 0.045f; - var textOptions = new TextOptions(font) - { - Origin = new SixLabors.ImageSharp.PointF(font.Size / 2.0f, font.Size / 2.0f + verticalOffset), - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center - }; - - image.Mutate(x => x.DrawText(textOptions, emoji, SixLabors.ImageSharp.Color.Black)); - } - - public static Image GetBitmap(string emoji, int iconSize) - { - int width = iconSize; - int height = iconSize; - - var bitmap = new Bitmap(width, height, PixelFormat.Format32bppArgb); - var bitmapData = bitmap.LockBits(new System.Drawing.Rectangle(0, 0, width, height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb); - - try - { - unsafe - { - var image = SixLabors.ImageSharp.Image.WrapMemory((void*)bitmapData.Scan0, width: width, height: height); - RenderEmoji(emoji, iconSize, image); - } - } - finally - { - bitmap.UnlockBits(bitmapData); - } - - return bitmap; - } - - public static BitmapSource GetBitmapSource(string emoji, int iconSize) - { - var pixelFormat = PixelFormats.Bgra32; - int width = iconSize; - int height = iconSize; - int stride = (width * pixelFormat.BitsPerPixel + 7) / 8; - byte[] pixels = new byte[stride * height]; - - var image = SixLabors.ImageSharp.Image.WrapMemory(byteMemory: pixels, width: width, height: height); - - RenderEmoji(emoji, iconSize, image); - - var source = BitmapSource.Create(pixelWidth: width, pixelHeight: height, dpiX: 96, dpiY: 96, pixelFormat: pixelFormat, palette: null, pixels: pixels, stride: stride); - source.Freeze(); - - return source; - } - } -} diff --git a/src/Greenshot.Editor/Drawing/Surface.cs b/src/Greenshot.Editor/Drawing/Surface.cs index efb3ebdd8..ccf979661 100644 --- a/src/Greenshot.Editor/Drawing/Surface.cs +++ b/src/Greenshot.Editor/Drawing/Surface.cs @@ -39,6 +39,7 @@ using Greenshot.Base.Interfaces; using Greenshot.Base.Interfaces.Drawing; using Greenshot.Base.Interfaces.Drawing.Adorners; using Greenshot.Editor.Configuration; +using Greenshot.Editor.Drawing.Emoji; using Greenshot.Editor.Drawing.Fields; using Greenshot.Editor.Memento; using log4net; diff --git a/src/Greenshot.Editor/Forms/ImageEditorForm.Designer.cs b/src/Greenshot.Editor/Forms/ImageEditorForm.Designer.cs index 34e7c55fc..2017a975c 100644 --- a/src/Greenshot.Editor/Forms/ImageEditorForm.Designer.cs +++ b/src/Greenshot.Editor/Forms/ImageEditorForm.Designer.cs @@ -22,9 +22,11 @@ using Greenshot.Base.Controls; using Greenshot.Editor.Controls; using Greenshot.Editor.Drawing; +using Greenshot.Editor.Drawing.Emoji; -namespace Greenshot.Editor.Forms { - partial class ImageEditorForm { +namespace Greenshot.Editor.Forms +{ + partial class ImageEditorForm { /// /// Designer variable used to keep track of non-visual components. /// diff --git a/src/Greenshot.Editor/Forms/ImageEditorForm.cs b/src/Greenshot.Editor/Forms/ImageEditorForm.cs index 4da3ec385..c4f368da5 100644 --- a/src/Greenshot.Editor/Forms/ImageEditorForm.cs +++ b/src/Greenshot.Editor/Forms/ImageEditorForm.cs @@ -45,7 +45,7 @@ using Greenshot.Base.Interfaces; using Greenshot.Base.Interfaces.Drawing; using Greenshot.Base.Interfaces.Forms; using Greenshot.Editor.Configuration; -using Greenshot.Editor.Controls; +using Greenshot.Editor.Controls.Emoji; using Greenshot.Editor.Destinations; using Greenshot.Editor.Drawing; using Greenshot.Editor.Drawing.Fields; diff --git a/src/Greenshot.Editor/Greenshot.Editor.csproj b/src/Greenshot.Editor/Greenshot.Editor.csproj index a343853f4..6a3069383 100644 --- a/src/Greenshot.Editor/Greenshot.Editor.csproj +++ b/src/Greenshot.Editor/Greenshot.Editor.csproj @@ -89,11 +89,13 @@ - + PreserveNewest - - + emoji-test.txt + + PreserveNewest - + TwemojiMozilla.ttf + \ No newline at end of file