mirror of
https://github.com/greenshot/greenshot
synced 2025-08-24 07:06:23 -07:00
Trying to make a bit more sense of the code, this is one of the first steps... didn't optimize anything yet.
This commit is contained in:
parent
9bea81ecda
commit
2f556b44d1
17 changed files with 533 additions and 408 deletions
69
src/Greenshot.Editor/Controls/Emoji/ChunkHelper.cs
Normal file
69
src/Greenshot.Editor/Controls/Emoji/ChunkHelper.cs
Normal file
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Greenshot.Editor.Controls.Emoji;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This seems to enumerate multiple IEnumerables.
|
||||||
|
/// TODO: Replace this with LINQ
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T"></typeparam>
|
||||||
|
internal sealed class ChunkHelper<T> : IEnumerable<IEnumerable<T>>
|
||||||
|
{
|
||||||
|
private readonly IEnumerable<T> _elements;
|
||||||
|
private readonly int _size;
|
||||||
|
private bool _hasMore;
|
||||||
|
|
||||||
|
public ChunkHelper(IEnumerable<T> elements, int size)
|
||||||
|
{
|
||||||
|
_elements = elements;
|
||||||
|
_size = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerator<IEnumerable<T>> GetEnumerator()
|
||||||
|
{
|
||||||
|
using var enumerator = _elements.GetEnumerator();
|
||||||
|
_hasMore = enumerator.MoveNext();
|
||||||
|
while (_hasMore)
|
||||||
|
{
|
||||||
|
yield return GetNextBatch(enumerator).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<T> GetNextBatch(IEnumerator<T> enumerator)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < _size; ++i)
|
||||||
|
{
|
||||||
|
yield return enumerator.Current;
|
||||||
|
_hasMore = enumerator.MoveNext();
|
||||||
|
if (!_hasMore)
|
||||||
|
{
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||||
|
}
|
54
src/Greenshot.Editor/Controls/Emoji/EmojiControl.cs
Normal file
54
src/Greenshot.Editor/Controls/Emoji/EmojiControl.cs
Normal file
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
199
src/Greenshot.Editor/Controls/Emoji/EmojiData.cs
Normal file
199
src/Greenshot.Editor/Controls/Emoji/EmojiData.cs
Normal file
|
@ -0,0 +1,199 @@
|
||||||
|
//
|
||||||
|
// Emoji.Wpf — Emoji support for WPF
|
||||||
|
//
|
||||||
|
// Copyright © 2017—2021 Sam Hocevar <sam@hocevar.net>
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This class processes the emoji-test.txt to generate a list of possible emoji depending ony skin tone and hairstyle.
|
||||||
|
/// </summary>
|
||||||
|
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<string> SkinToneComponents = new List<string>
|
||||||
|
{
|
||||||
|
"🏻", // light skin tone
|
||||||
|
"🏼", // medium-light skin tone
|
||||||
|
"🏽", // medium skin tone
|
||||||
|
"🏾", // medium-dark skin tone
|
||||||
|
"🏿", // dark skin tone
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly List<string> HairStyleComponents = new List<string>
|
||||||
|
{
|
||||||
|
"🦰", // 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<string, Emojis.Emoji>();
|
||||||
|
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<string, string>();
|
||||||
|
var allText = new List<string>();
|
||||||
|
|
||||||
|
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<string> 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,7 +4,7 @@
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
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"
|
Orientation="Horizontal"
|
||||||
mc:Ignorable="d">
|
mc:Ignorable="d">
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@
|
||||||
<Button Background="Transparent" BorderBrush="Transparent"
|
<Button Background="Transparent" BorderBrush="Transparent"
|
||||||
Margin="0" Click="OnEmojiPicked"
|
Margin="0" Click="OnEmojiPicked"
|
||||||
Width="40" Height="40" ToolTip="{Binding Path=Name}">
|
Width="40" Height="40" ToolTip="{Binding Path=Name}">
|
||||||
<controls:EmojiControl Height="24" Emoji="{Binding Path=Text}" />
|
<emoji:EmojiControl Height="24" Emoji="{Binding Path=Text}" />
|
||||||
</Button>
|
</Button>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ListView.ItemTemplate>
|
</ListView.ItemTemplate>
|
||||||
|
@ -48,7 +48,7 @@
|
||||||
x:Name="VariationButton" Background="Transparent" BorderBrush="Transparent"
|
x:Name="VariationButton" Background="Transparent" BorderBrush="Transparent"
|
||||||
Click="OnEmojiPicked" Focusable="False" ToolTip="{Binding Path=Name}">
|
Click="OnEmojiPicked" Focusable="False" ToolTip="{Binding Path=Name}">
|
||||||
<Grid>
|
<Grid>
|
||||||
<controls:EmojiControl Height="24" Margin="4" Emoji="{Binding Path=Text}"
|
<emoji:EmojiControl Height="24" Margin="4" Emoji="{Binding Path=Text}"
|
||||||
VerticalAlignment="Center" HorizontalAlignment="Center"/>
|
VerticalAlignment="Center" HorizontalAlignment="Center"/>
|
||||||
<Polygon Visibility="{Binding HasVariations, Converter={StaticResource BoolToVis}}"
|
<Polygon Visibility="{Binding HasVariations, Converter={StaticResource BoolToVis}}"
|
||||||
Width="6" Height="6" VerticalAlignment="Bottom" HorizontalAlignment="Right"
|
Width="6" Height="6" VerticalAlignment="Bottom" HorizontalAlignment="Right"
|
||||||
|
@ -64,7 +64,7 @@
|
||||||
</StackPanel.Resources>
|
</StackPanel.Resources>
|
||||||
|
|
||||||
<ToggleButton x:Name="Button_INTERNAL">
|
<ToggleButton x:Name="Button_INTERNAL">
|
||||||
<controls:EmojiControl x:Name="Image" Emoji="{Binding ElementName=StackPanel_INTERNAL, Mode=OneWay, Path=Selection, UpdateSourceTrigger=PropertyChanged}"/>
|
<emoji:EmojiControl x:Name="Image" Emoji="{Binding ElementName=StackPanel_INTERNAL, Mode=OneWay, Path=Selection, UpdateSourceTrigger=PropertyChanged}"/>
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
|
|
||||||
<Popup IsOpen="{Binding IsChecked, ElementName=Button_INTERNAL, Mode=TwoWay}"
|
<Popup IsOpen="{Binding IsChecked, ElementName=Button_INTERNAL, Mode=TwoWay}"
|
||||||
|
@ -77,11 +77,11 @@
|
||||||
<TabControl.ItemTemplate>
|
<TabControl.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<Grid>
|
<Grid>
|
||||||
<controls:EmojiControl Height="24" Emoji="{Binding Icon}" >
|
<emoji:EmojiControl Height="24" Emoji="{Binding Icon}" >
|
||||||
<Image.ToolTip>
|
<Image.ToolTip>
|
||||||
<TextBlock Text="{Binding Name}"/>
|
<TextBlock Text="{Binding Name}"/>
|
||||||
</Image.ToolTip>
|
</Image.ToolTip>
|
||||||
</controls:EmojiControl>
|
</emoji:EmojiControl>
|
||||||
</Grid>
|
</Grid>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</TabControl.ItemTemplate>
|
</TabControl.ItemTemplate>
|
|
@ -17,6 +17,7 @@ using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
using System.Windows.Controls.Primitives;
|
using System.Windows.Controls.Primitives;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
|
using Greenshot.Editor.Controls.Emoji;
|
||||||
|
|
||||||
namespace Greenshot.Editor.Controls
|
namespace Greenshot.Editor.Controls
|
||||||
{
|
{
|
70
src/Greenshot.Editor/Controls/Emoji/Emojis.cs
Normal file
70
src/Greenshot.Editor/Controls/Emoji/Emojis.cs
Normal file
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<Group> Groups { get; set; } = new();
|
||||||
|
|
||||||
|
public class Group
|
||||||
|
{
|
||||||
|
[XmlAttribute]
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
[XmlElement(ElementName = "Group")]
|
||||||
|
public List<Group> SubGroups { get; set; } = new();
|
||||||
|
|
||||||
|
[XmlElement(ElementName = "Emoji")]
|
||||||
|
public List<Emoji> Emojis { get; set; } = new();
|
||||||
|
|
||||||
|
public IEnumerable<IEnumerable<Emoji>> EmojiChunkList => new ChunkHelper<Emoji>(EmojiList, 8);
|
||||||
|
|
||||||
|
public string Icon => SubGroups.FirstOrDefault()?.Emojis.FirstOrDefault()?.Text;
|
||||||
|
|
||||||
|
public IEnumerable<Emoji> 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<Emoji> Variations { get; set; } = new();
|
||||||
|
|
||||||
|
public bool HasVariations => Variations.Count > 0;
|
||||||
|
|
||||||
|
public IEnumerable<Emoji> AllVariations => HasVariations ? new[] { this }.Union(Variations) : Array.Empty<Emoji>();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,262 +0,0 @@
|
||||||
//
|
|
||||||
// Emoji.Wpf — Emoji support for WPF
|
|
||||||
//
|
|
||||||
// Copyright © 2017—2021 Sam Hocevar <sam@hocevar.net>
|
|
||||||
//
|
|
||||||
// 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<string> SkinToneComponents = new List<string>
|
|
||||||
{
|
|
||||||
"🏻", // light skin tone
|
|
||||||
"🏼", // medium-light skin tone
|
|
||||||
"🏽", // medium skin tone
|
|
||||||
"🏾", // medium-dark skin tone
|
|
||||||
"🏿", // dark skin tone
|
|
||||||
};
|
|
||||||
|
|
||||||
private static List<string> HairStyleComponents = new List<string>
|
|
||||||
{
|
|
||||||
"🦰", // 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<string, Emojis.Emoji>();
|
|
||||||
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<string, string>();
|
|
||||||
var alltext = new List<string>();
|
|
||||||
|
|
||||||
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<string> 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<Group> Groups { get; set; } = new();
|
|
||||||
|
|
||||||
public class Group
|
|
||||||
{
|
|
||||||
[XmlAttribute]
|
|
||||||
public string Name { get; set; }
|
|
||||||
|
|
||||||
[XmlElement(ElementName = "Group")]
|
|
||||||
public List<Group> SubGroups { get; set; } = new();
|
|
||||||
|
|
||||||
[XmlElement(ElementName = "Emoji")]
|
|
||||||
public List<Emoji> Emojis { get; set; } = new();
|
|
||||||
|
|
||||||
public IEnumerable<IEnumerable<Emoji>> EmojiChunkList => new ChunkHelper<Emoji>(EmojiList, 8);
|
|
||||||
|
|
||||||
public string Icon => SubGroups.FirstOrDefault()?.Emojis.FirstOrDefault()?.Text;
|
|
||||||
|
|
||||||
public IEnumerable<Emoji> 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<Emoji> Variations { get; set; }
|
|
||||||
|
|
||||||
public bool HasVariations => Variations.Count > 0;
|
|
||||||
|
|
||||||
public IEnumerable<Emoji> AllVariations => HasVariations ? new[] { this }.Union(Variations) : Array.Empty<Emoji>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class ChunkHelper<T> : IEnumerable<IEnumerable<T>>
|
|
||||||
{
|
|
||||||
public ChunkHelper(IEnumerable<T> elements, int size)
|
|
||||||
{
|
|
||||||
m_elements = elements;
|
|
||||||
m_size = size;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerator<IEnumerable<T>> GetEnumerator()
|
|
||||||
{
|
|
||||||
using var enumerator = m_elements.GetEnumerator();
|
|
||||||
m_has_more = enumerator.MoveNext();
|
|
||||||
while (m_has_more)
|
|
||||||
yield return GetNextBatch(enumerator).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<T> GetNextBatch(IEnumerator<T> 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<T> m_elements;
|
|
||||||
private readonly int m_size;
|
|
||||||
private bool m_has_more;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -30,13 +30,13 @@ using Greenshot.Editor.Controls;
|
||||||
using Greenshot.Editor.Helpers;
|
using Greenshot.Editor.Helpers;
|
||||||
using Image = System.Drawing.Image;
|
using Image = System.Drawing.Image;
|
||||||
|
|
||||||
namespace Greenshot.Editor.Drawing
|
namespace Greenshot.Editor.Drawing.Emoji
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Description of EmojiContainer.
|
/// Description of EmojiContainer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Serializable]
|
[Serializable]
|
||||||
public class EmojiContainer : VectorGraphicsContainer, IEmojiContainer, IHaveScaleOptions
|
public sealed class EmojiContainer : VectorGraphicsContainer, IEmojiContainer, IHaveScaleOptions
|
||||||
{
|
{
|
||||||
[NonSerialized] private static EmojiContainer _currentContainer;
|
[NonSerialized] private static EmojiContainer _currentContainer;
|
||||||
[NonSerialized] private static ElementHost _emojiPickerHost;
|
[NonSerialized] private static ElementHost _emojiPickerHost;
|
120
src/Greenshot.Editor/Drawing/Emoji/EmojiRenderer.cs
Normal file
120
src/Greenshot.Editor/Drawing/Emoji/EmojiRenderer.cs
Normal file
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This will render Emoji
|
||||||
|
/// </summary>
|
||||||
|
internal static class EmojiRenderer
|
||||||
|
{
|
||||||
|
private static readonly FontCollection _fontCollection = new FontCollection();
|
||||||
|
|
||||||
|
private static readonly Lazy<FontFamily> _twemojiFontFamily = new Lazy<FontFamily>(() =>
|
||||||
|
{
|
||||||
|
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<Rgba32> GetImage(string emoji, int iconSize)
|
||||||
|
{
|
||||||
|
var image = new Image<Rgba32>(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<Bgra32>((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<Bgra32>(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<FontFamily> _twemoji = new Lazy<FontFamily>(() =>
|
|
||||||
{
|
|
||||||
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<Rgba32> GetImage(string emoji, int iconSize)
|
|
||||||
{
|
|
||||||
var image = new Image<Rgba32>(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<Bgra32>((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<Bgra32>(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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -39,6 +39,7 @@ using Greenshot.Base.Interfaces;
|
||||||
using Greenshot.Base.Interfaces.Drawing;
|
using Greenshot.Base.Interfaces.Drawing;
|
||||||
using Greenshot.Base.Interfaces.Drawing.Adorners;
|
using Greenshot.Base.Interfaces.Drawing.Adorners;
|
||||||
using Greenshot.Editor.Configuration;
|
using Greenshot.Editor.Configuration;
|
||||||
|
using Greenshot.Editor.Drawing.Emoji;
|
||||||
using Greenshot.Editor.Drawing.Fields;
|
using Greenshot.Editor.Drawing.Fields;
|
||||||
using Greenshot.Editor.Memento;
|
using Greenshot.Editor.Memento;
|
||||||
using log4net;
|
using log4net;
|
||||||
|
|
|
@ -22,8 +22,10 @@
|
||||||
using Greenshot.Base.Controls;
|
using Greenshot.Base.Controls;
|
||||||
using Greenshot.Editor.Controls;
|
using Greenshot.Editor.Controls;
|
||||||
using Greenshot.Editor.Drawing;
|
using Greenshot.Editor.Drawing;
|
||||||
|
using Greenshot.Editor.Drawing.Emoji;
|
||||||
|
|
||||||
namespace Greenshot.Editor.Forms {
|
namespace Greenshot.Editor.Forms
|
||||||
|
{
|
||||||
partial class ImageEditorForm {
|
partial class ImageEditorForm {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Designer variable used to keep track of non-visual components.
|
/// Designer variable used to keep track of non-visual components.
|
||||||
|
|
|
@ -45,7 +45,7 @@ using Greenshot.Base.Interfaces;
|
||||||
using Greenshot.Base.Interfaces.Drawing;
|
using Greenshot.Base.Interfaces.Drawing;
|
||||||
using Greenshot.Base.Interfaces.Forms;
|
using Greenshot.Base.Interfaces.Forms;
|
||||||
using Greenshot.Editor.Configuration;
|
using Greenshot.Editor.Configuration;
|
||||||
using Greenshot.Editor.Controls;
|
using Greenshot.Editor.Controls.Emoji;
|
||||||
using Greenshot.Editor.Destinations;
|
using Greenshot.Editor.Destinations;
|
||||||
using Greenshot.Editor.Drawing;
|
using Greenshot.Editor.Drawing;
|
||||||
using Greenshot.Editor.Drawing.Fields;
|
using Greenshot.Editor.Drawing.Fields;
|
||||||
|
|
|
@ -89,11 +89,13 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Update="Resources\emoji-test.txt" Link="%(LinkBase)\%(Filename)%(Extension)">
|
<ContentWithTargetPath Include="Controls\Emoji\emoji-test.txt">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</None>
|
<TargetPath>emoji-test.txt</TargetPath>
|
||||||
<None Update="Resources\TwemojiMozilla.ttf" Link="%(LinkBase)\%(Filename)%(Extension)">
|
</ContentWithTargetPath>
|
||||||
|
<ContentWithTargetPath Include="Drawing\Emoji\TwemojiMozilla.ttf">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</None>
|
<TargetPath>TwemojiMozilla.ttf</TargetPath>
|
||||||
|
</ContentWithTargetPath>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
Loading…
Add table
Add a link
Reference in a new issue