Added gz support, and improved upon some additional code.

This commit is contained in:
Robin Krom 2022-10-25 21:48:40 +02:00
commit 84be12b826
No known key found for this signature in database
GPG key ID: BCC01364F1371490
7 changed files with 148 additions and 183 deletions

View file

@ -10,4 +10,22 @@
<Message Text="Processing: $(ProjectDir)$(ProjectName).Credentials.template" Importance="high"/> <Message Text="Processing: $(ProjectDir)$(ProjectName).Credentials.template" Importance="high"/>
<TemplateFile Template="$(ProjectDir)$(ProjectName).Credentials.template" OutputFilename="$(ProjectDir)$(ProjectName).Credentials.cs" Tokens="@(Tokens)" /> <TemplateFile Template="$(ProjectDir)$(ProjectName).Credentials.template" OutputFilename="$(ProjectDir)$(ProjectName).Credentials.cs" Tokens="@(Tokens)" />
</Target> </Target>
<UsingTask TaskName="GZipTask" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
<ParameterGroup>
<InputFile ParameterType="System.String" Required="true" />
<OutputFile ParameterType="System.String" Required="true" />
</ParameterGroup>
<Task>
<Reference Include="System.IO.Compression" />
<Using Namespace="System.IO.Compression" />
<Code Type="Fragment" Language="cs"><![CDATA[
Directory.CreateDirectory(Path.GetDirectoryName(OutputFile));
var data = File.ReadAllBytes(InputFile);
using (var s = new FileStream(OutputFile, FileMode.Create))
using (var gs = new GZipStream(s, CompressionMode.Compress, false))
gs.Write(data, 0, data.Length);
]]></Code>
</Task>
</UsingTask>
</Project> </Project>

View file

@ -1,69 +0,0 @@
/*
* 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();
}

View file

@ -11,15 +11,18 @@
// //
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Xml; using System.Xml;
using System.Xml.Serialization; using System.Xml.Serialization;
#if DEBUG
using System.Collections.Generic;
using System.IO.Compression;
using System.Linq;
using System.Reflection;
using System.Text;
using SixLabors.Fonts.Unicode; using SixLabors.Fonts.Unicode;
#endif
namespace Greenshot.Editor.Controls.Emoji namespace Greenshot.Editor.Controls.Emoji
{ {
@ -28,27 +31,17 @@ namespace Greenshot.Editor.Controls.Emoji
/// </summary> /// </summary>
public static class EmojiData public static class EmojiData
{ {
private const string FilePath = "emojis.xml"; private const string EmojisXmlFilePath = "emojis.xml";
private const string EmojisTestFile = @"emoji-test.txt.gz";
#if DEBUG
private const string Adult = "(👨|👩)(🏻|🏼|🏽|🏾|🏿)?";
private const string Child = "(👦|👧|👶)(🏻|🏼|🏽|🏾|🏿)?";
private static readonly Regex MatchFamily = new($"{Adult}(\u200d{Adult})*(\u200d{Child})+");
public static Emojis Data { get; private set; } = new Emojis(); private static readonly Regex MatchGroup = new(@"^# group: (.*)", RegexOptions.Compiled);
private static readonly Regex MatchSubgroup = new(@"^# subgroup: (.*)", RegexOptions.Compiled);
public static void Load() private static readonly Regex MatchSequence = new(@"^([0-9a-fA-F ]+[0-9a-fA-F]).*; *([-a-z]*) *# [^ ]* (E[0-9.]* )?(.*)", RegexOptions.Compiled);
{ private static readonly List<string> SkinToneComponents = new()
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 "🏻", // light skin tone
"🏼", // medium-light skin tone "🏼", // medium-light skin tone
@ -57,7 +50,7 @@ namespace Greenshot.Editor.Controls.Emoji
"🏿", // dark skin tone "🏿", // dark skin tone
}; };
private static readonly List<string> HairStyleComponents = new List<string> private static readonly List<string> HairStyleComponents = new()
{ {
"🦰", // red hair "🦰", // red hair
"🦱", // curly hair "🦱", // curly hair
@ -65,21 +58,43 @@ namespace Greenshot.Editor.Controls.Emoji
"🦲", // bald "🦲", // bald
}; };
private static string ToColonSyntax(string s) private static readonly Regex MatchSkinTone = new($"({string.Join("|", SkinToneComponents)})", RegexOptions.Compiled);
=> Regex.Replace(s.Trim().ToLowerInvariant(), "[^a-z0-9]+", "-"); private static readonly Regex MatchHairStyle = new($"({string.Join("|", HairStyleComponents)})", RegexOptions.Compiled);
#endif
public static Emojis Data { get; private set; } = new();
public static void Load()
{
var x = new XmlSerializer(typeof(Emojis));
if (File.Exists(EmojisXmlFilePath))
{
Data = (Emojis)x.Deserialize(new XmlTextReader(EmojisXmlFilePath));
}
#if RELEASE
else
{
throw new NotSupportedException($"Missing {EmojisXmlFilePath}, can't load ");
}
#elif DEBUG
else
{
// To be removed
ParseEmojiList();
x.Serialize(new XmlTextWriter(EmojisXmlFilePath, Encoding.UTF8), Data);
}
#endif
}
#if DEBUG
private static string ToColonSyntax(string s) => Regex.Replace(s.Trim().ToLowerInvariant(), "[^a-z0-9]+", "-");
private static void ParseEmojiList() private static void ParseEmojiList()
{ {
var lookupByName = new Dictionary<string, Emojis.Emoji>(); 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 qualifiedLut = new Dictionary<string, string>();
var allText = new List<string>(); var allText = new List<string>();
@ -89,7 +104,7 @@ namespace Greenshot.Editor.Controls.Emoji
foreach (var line in EmojiDescriptionLines()) foreach (var line in EmojiDescriptionLines())
{ {
var m = matchGroup.Match(line); var m = MatchGroup.Match(line);
if (m.Success) if (m.Success)
{ {
currentGroup = new Emojis.Group { Name = m.Groups[1].ToString() }; currentGroup = new Emojis.Group { Name = m.Groups[1].ToString() };
@ -97,85 +112,82 @@ namespace Greenshot.Editor.Controls.Emoji
continue; continue;
} }
m = matchSubgroup.Match(line); m = MatchSubgroup.Match(line);
if (m.Success) if (m.Success)
{ {
currentSubgroup = new Emojis.Group { Name = m.Groups[1].ToString() }; currentSubgroup = new Emojis.Group { Name = m.Groups[1].ToString() };
currentGroup.SubGroups.Add(currentSubgroup); currentGroup?.SubGroups?.Add(currentSubgroup);
continue; continue;
} }
m = matchSequence.Match(line); m = MatchSequence.Match(line);
if (m.Success) if (!m.Success)
{ {
string sequence = m.Groups[1].ToString(); continue;
string name = m.Groups[4].ToString(); }
string sequence = m.Groups[1].ToString();
string name = m.Groups[4].ToString();
string text = string.Join("", from n in sequence.Split(' ') string text = string.Join("", sequence.Split(' ').Select(c => char.ConvertFromUtf32(Convert.ToInt32(c, 16))));
select char.ConvertFromUtf32(Convert.ToInt32(n, 16))); bool hasModifier = false;
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.
// If this is a family emoji, no need to add it to our big matching if (!MatchFamily.Match(text).Success)
// regex, since the match_family regex is already included. {
} // Construct a regex to replace e.g. "🏻" with "(🏻|🏼|🏽|🏾|🏿)" in a big
else // regex so that we can match all variations of this Emoji even if they are
{ // not in the standard.
// Construct a regex to replace e.g. "🏻" with "(🏻|🏼|🏽|🏾|🏿)" in a big bool hasNonfirstModifier = false;
// regex so that we can match all variations of this Emoji even if they are var regexText = MatchSkinTone.Replace(
// not in the standard. MatchHairStyle.Replace(text, (x) =>
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); hasModifier = true;
} hasNonfirstModifier |= x.Value != HairStyleComponents[0];
} return MatchHairStyle.ToString();
}), (x) =>
{
hasModifier = true;
hasNonfirstModifier |= x.Value != SkinToneComponents[0];
return MatchSkinTone.ToString();
});
// If there is already a differently-qualified version of this character, skip it. if (!hasNonfirstModifier)
// FIXME: this only works well if fully-qualified appears first in the list.
var unqualified = text.Replace("\ufe0f", "");
if (qualifiedLut.ContainsKey(unqualified))
{ {
continue; allText.Add(hasModifier ? regexText : text);
} }
}
// Fix simple fully-qualified emojis // If there is already a differently-qualified version of this character, skip it.
if (CodePoint.GetCodePointCount(text.AsSpan()) == 2) // FIXME: this only works well if fully-qualified appears first in the list.
{ var unqualified = text.Replace("\ufe0f", "");
text = text.TrimEnd('\ufe0f'); if (qualifiedLut.ContainsKey(unqualified))
} {
continue;
}
qualifiedLut[unqualified] = text; // Fix simple fully-qualified emojis
if (CodePoint.GetCodePointCount(text.AsSpan()) == 2)
{
text = text.TrimEnd('\ufe0f');
}
var emoji = new Emojis.Emoji { Name = name, Text = text, }; qualifiedLut[unqualified] = text;
lookupByName[ToColonSyntax(name)] = emoji; var emoji = new Emojis.Emoji { Name = name, Text = text};
// Get the left part of the name and check whether were a variation of an existing lookupByName[ToColonSyntax(name)] = emoji;
// 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 // Get the left part of the name and check whether were a variation of an existing
if (name.Contains(":") && lookupByName.TryGetValue(ToColonSyntax(name.Split(':')[0]), out var parent_emoji)) // 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
parent_emoji.Variations.Add(emoji); if (name.Contains(":") && lookupByName.TryGetValue(ToColonSyntax(name.Split(':')[0]), out var parentEmoji))
} {
else parentEmoji.Variations.Add(emoji);
{ }
currentSubgroup.Emojis.Add(emoji); else
} {
currentSubgroup?.Emojis?.Add(emoji);
} }
} }
@ -186,14 +198,16 @@ namespace Greenshot.Editor.Controls.Emoji
private static IEnumerable<string> EmojiDescriptionLines() private static IEnumerable<string> EmojiDescriptionLines()
{ {
var exeDirectory = Path.GetDirectoryName(Assembly.GetCallingAssembly().Location); var exeDirectory = Path.GetDirectoryName(Assembly.GetCallingAssembly().Location);
var emojiTestFile = Path.Combine(exeDirectory, @"emoji-test.txt"); var emojiTestFile = Path.Combine(exeDirectory, EmojisTestFile);
if (!File.Exists(emojiTestFile)) if (!File.Exists(emojiTestFile))
{ {
throw new FileNotFoundException($"Can't find {emojiTestFile}, bad installation?"); throw new FileNotFoundException($"Can't find {emojiTestFile}, bad installation?");
} }
using var fileStream = new FileStream(emojiTestFile, FileMode.Open, FileAccess.Read); using var fileStream = new FileStream(emojiTestFile, FileMode.Open, FileAccess.Read);
using var gzStream = new GZipStream(fileStream, CompressionMode.Decompress);
using var streamReader = new StreamReader(fileStream); using var streamReader = new StreamReader(fileStream);
return streamReader.ReadToEnd().Split('\r', '\n'); return streamReader.ReadToEnd().Split('\r', '\n');
} }
#endif
} }
} }

View file

@ -42,14 +42,7 @@ public class Emojis
[XmlElement(ElementName = "Emoji")] [XmlElement(ElementName = "Emoji")]
public List<Emoji> Emojis { get; set; } = new(); 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 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 public class Emoji

View file

@ -153,7 +153,10 @@ namespace Greenshot.Editor.Drawing.Emoji
protected override Image ComputeBitmap() protected override Image ComputeBitmap()
{ {
var iconSize = Math.Min(Bounds.Width, Bounds.Height); var iconSize = Math.Min(Bounds.Width, Bounds.Height);
if (iconSize <= 0) return null; if (iconSize <= 0)
{
return null;
}
var image = EmojiRenderer.GetBitmap(Emoji, iconSize); var image = EmojiRenderer.GetBitmap(Emoji, iconSize);
if (RotationAngle != 0) if (RotationAngle != 0)

View file

@ -21,6 +21,7 @@
using System; using System;
using System.IO; using System.IO;
using System.IO.Compression;
using System.Reflection; using System.Reflection;
using SixLabors.Fonts; using SixLabors.Fonts;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
@ -35,9 +36,9 @@ namespace Greenshot.Editor.Drawing.Emoji
/// </summary> /// </summary>
internal static class EmojiRenderer internal static class EmojiRenderer
{ {
private static readonly FontCollection _fontCollection = new FontCollection(); private static readonly FontCollection TwemojiFontCollection = new();
private static readonly Lazy<FontFamily> _twemojiFontFamily = new Lazy<FontFamily>(() => private static readonly Lazy<FontFamily> TwemojiFontFamily = new(() =>
{ {
var exeDirectory = Path.GetDirectoryName(Assembly.GetCallingAssembly().Location); var exeDirectory = Path.GetDirectoryName(Assembly.GetCallingAssembly().Location);
var twemojiFontFile = Path.Combine(exeDirectory, "TwemojiMozilla.ttf"); var twemojiFontFile = Path.Combine(exeDirectory, "TwemojiMozilla.ttf");
@ -47,8 +48,8 @@ namespace Greenshot.Editor.Drawing.Emoji
} }
using var fileStream = new FileStream(twemojiFontFile, FileMode.Open, FileAccess.Read); using var fileStream = new FileStream(twemojiFontFile, FileMode.Open, FileAccess.Read);
_fontCollection.Add(fileStream); TwemojiFontCollection.Add(fileStream);
_fontCollection.TryGet("Twemoji Mozilla", out var fontFamily); TwemojiFontCollection.TryGet("Twemoji Mozilla", out var fontFamily);
return fontFamily; return fontFamily;
}); });
@ -62,7 +63,7 @@ namespace Greenshot.Editor.Drawing.Emoji
private static void RenderEmoji(string emoji, int iconSize, Image image) private static void RenderEmoji(string emoji, int iconSize, Image image)
{ {
var fontFamily = _twemojiFontFamily.Value; var fontFamily = TwemojiFontFamily.Value;
var font = fontFamily.CreateFont(iconSize, FontStyle.Regular); var font = fontFamily.CreateFont(iconSize, FontStyle.Regular);
var verticalOffset = font.Size * 0.045f; var verticalOffset = font.Size * 0.045f;
var textOptions = new TextOptions(font) var textOptions = new TextOptions(font)

View file

@ -15,6 +15,11 @@
<ProjectReference Include="..\Greenshot.Base\Greenshot.Base.csproj" /> <ProjectReference Include="..\Greenshot.Base\Greenshot.Base.csproj" />
</ItemGroup> </ItemGroup>
<Target Name="CompressEmojiTxtResource" BeforeTargets="BeforeBuild" Condition="!Exists('Controls\Emoji\emoji-test.txt.gz')">
<GZipTask InputFile="Controls\Emoji\emoji-test.txt"
OutputFile="Controls\Emoji\emoji-test.txt.gz" />
</Target>
<ItemGroup> <ItemGroup>
<Compile Update="Controls\BindableToolStripButton.cs"> <Compile Update="Controls\BindableToolStripButton.cs">
<SubType>Component</SubType> <SubType>Component</SubType>
@ -89,9 +94,9 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ContentWithTargetPath Include="Controls\Emoji\emoji-test.txt"> <ContentWithTargetPath Include="Controls\Emoji\emoji-test.txt.gz">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<TargetPath>emoji-test.txt</TargetPath> <TargetPath>emoji-test.txt.gz</TargetPath>
</ContentWithTargetPath> </ContentWithTargetPath>
<ContentWithTargetPath Include="Drawing\Emoji\TwemojiMozilla.ttf"> <ContentWithTargetPath Include="Drawing\Emoji\TwemojiMozilla.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>