refactor: move to net5

This commit is contained in:
Anna 2021-08-24 01:43:09 -04:00
parent d9e30396f4
commit 581b469d79
Signed by: anna
GPG Key ID: 0B391D8F06FCD9E0
16 changed files with 194 additions and 86 deletions

View File

@ -1,11 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net48</TargetFramework>
<TargetFramework>net5-windows</TargetFramework>
<Version>1.3.4</Version>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
<ItemGroup>
@ -13,6 +15,10 @@
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Dalamud.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="FFXIVClientStructs">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\FFXIVClientStructs.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="ImGui.NET">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\ImGui.NET.dll</HintPath>
<Private>False</Private>
@ -29,10 +35,12 @@
<ItemGroup>
<PackageReference Include="AngleSharp" Version="1.0.0-alpha-844"/>
<PackageReference Include="DalamudPackager" Version="1.2.1"/>
<PackageReference Include="Fody" Version="6.5.2" PrivateAssets="all"/>
<PackageReference Include="ILMerge.Fody" Version="1.16.0" PrivateAssets="all"/>
<PackageReference Include="DalamudPackager" Version="2.1.2"/>
<PackageReference Include="Nager.PublicSuffix" Version="2.2.2"/>
</ItemGroup>
<ItemGroup>
<Content Include="..\icon.png" Link="images/icon.png" CopyToOutputDirectory="PreserveNewest" Visible="false"/>
</ItemGroup>
</Project>

View File

@ -1,5 +1,6 @@
author: ascclemens
name: Expanded Search Info
punchline: Displays extra information pulled from search info when examining.
description: |-
Displays extra information pulled from search info when examining.
@ -12,4 +13,6 @@ description: |-
Simply examine someone with a search info containing pointers to one of
these locations and the plugin will display information automatically.
Icon: expand by Gregor Cresnar from the Noun Project
repo_url: https://git.sr.ht/~jkcclemens/ExpandedSearchInfo

View File

@ -1,3 +0,0 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ILMerge/>
</Weavers>

View File

@ -1,8 +1,7 @@
using System;
using System.Runtime.InteropServices;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Hooking;
using Dalamud.Plugin;
using Dalamud.Logging;
namespace ExpandedSearchInfo {
public class GameFunctions : IDisposable {
@ -12,15 +11,15 @@ namespace ExpandedSearchInfo {
private readonly Hook<SearchInfoDownloadedDelegate>? _searchInfoDownloadedHook;
internal delegate void ReceiveSearchInfoEventDelegate(int actorId, SeString info);
internal delegate void ReceiveSearchInfoEventDelegate(uint objectId, SeString info);
internal event ReceiveSearchInfoEventDelegate? ReceiveSearchInfo;
internal GameFunctions(Plugin plugin) {
this.Plugin = plugin;
var sidPtr = this.Plugin.Interface.TargetModuleScanner.ScanText("48 89 5C 24 ?? 48 89 6C 24 ?? 56 48 83 EC 20 49 8B E8 8B DA");
this._searchInfoDownloadedHook = new Hook<SearchInfoDownloadedDelegate>(sidPtr, new SearchInfoDownloadedDelegate(this.SearchInfoDownloaded));
var sidPtr = this.Plugin.SigScanner.ScanText("48 89 5C 24 ?? 48 89 6C 24 ?? 56 48 83 EC 20 49 8B E8 8B DA");
this._searchInfoDownloadedHook = new Hook<SearchInfoDownloadedDelegate>(sidPtr, this.SearchInfoDownloaded);
this._searchInfoDownloadedHook.Enable();
}
@ -28,14 +27,14 @@ namespace ExpandedSearchInfo {
this._searchInfoDownloadedHook?.Dispose();
}
private byte SearchInfoDownloaded(IntPtr data, IntPtr a2, IntPtr searchInfoPtr, IntPtr a4) {
private unsafe byte SearchInfoDownloaded(IntPtr data, IntPtr a2, IntPtr searchInfoPtr, IntPtr a4) {
var result = this._searchInfoDownloadedHook!.Original(data, a2, searchInfoPtr, a4);
try {
// Updated: 4.5
var actorId = Marshal.ReadInt32(data + 48);
var actorId = *(uint*) (data + 48);
var searchInfo = this.Plugin.Interface.SeStringManager.ReadRawSeString(searchInfoPtr);
var searchInfo = this.Plugin.SeStringManager.ReadRawSeString(searchInfoPtr);
this.ReceiveSearchInfo?.Invoke(actorId, searchInfo);
} catch (Exception ex) {

View File

@ -1,4 +1,9 @@
using Dalamud.Game.Command;
using Dalamud.Game;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.Command;
using Dalamud.Game.Gui;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.IoC;
using Dalamud.Plugin;
namespace ExpandedSearchInfo {
@ -6,15 +11,30 @@ namespace ExpandedSearchInfo {
public class Plugin : IDalamudPlugin {
public string Name => "Expanded Search Info";
internal PluginConfiguration Config { get; private set; } = null!;
internal DalamudPluginInterface Interface { get; private set; } = null!;
internal GameFunctions Functions { get; private set; } = null!;
internal SearchInfoRepository Repository { get; private set; } = null!;
private PluginUi Ui { get; set; } = null!;
[PluginService]
internal DalamudPluginInterface Interface { get; init; } = null!;
public void Initialize(DalamudPluginInterface pluginInterface) {
this.Interface = pluginInterface;
[PluginService]
internal CommandManager CommandManager { get; init; } = null!;
[PluginService]
internal GameGui GameGui { get; init; } = null!;
[PluginService]
internal ObjectTable ObjectTable { get; init; } = null!;
[PluginService]
internal SeStringManager SeStringManager { get; init; } = null!;
[PluginService]
internal SigScanner SigScanner { get; init; } = null!;
internal PluginConfiguration Config { get; }
internal GameFunctions Functions { get; }
internal SearchInfoRepository Repository { get; }
private PluginUi Ui { get; }
public Plugin() {
this.Config = (PluginConfiguration?) this.Interface.GetPluginConfig() ?? new PluginConfiguration();
this.Config.Initialise(this);
@ -22,13 +42,13 @@ namespace ExpandedSearchInfo {
this.Repository = new SearchInfoRepository(this);
this.Ui = new PluginUi(this);
this.Interface.CommandManager.AddHandler("/esi", new CommandInfo(this.OnCommand) {
this.CommandManager.AddHandler("/esi", new CommandInfo(this.OnCommand) {
HelpMessage = "Toggles Expanded Search Info's configuration window",
});
}
public void Dispose() {
this.Interface.CommandManager.RemoveHandler("/esi");
this.CommandManager.RemoveHandler("/esi");
this.Ui.Dispose();
this.Repository.Dispose();
this.Functions.Dispose();

View File

@ -2,6 +2,7 @@
using System.Diagnostics;
using System.Numerics;
using Dalamud.Interface;
using FFXIVClientStructs.FFXIV.Component.GUI;
using ImGuiNET;
namespace ExpandedSearchInfo {
@ -18,17 +19,17 @@ namespace ExpandedSearchInfo {
internal PluginUi(Plugin plugin) {
this.Plugin = plugin;
this.Plugin.Interface.UiBuilder.OnBuildUi += this.Draw;
this.Plugin.Interface.UiBuilder.OnOpenConfigUi += this.OnOpenConfigUi;
this.Plugin.Interface.UiBuilder.Draw += this.Draw;
this.Plugin.Interface.UiBuilder.OpenConfigUi += this.OnOpenConfigUi;
}
private void OnOpenConfigUi(object sender, EventArgs e) {
private void OnOpenConfigUi() {
this.ConfigVisible = true;
}
public void Dispose() {
this.Plugin.Interface.UiBuilder.OnOpenConfigUi -= this.OnOpenConfigUi;
this.Plugin.Interface.UiBuilder.OnBuildUi -= this.Draw;
this.Plugin.Interface.UiBuilder.OpenConfigUi -= this.OnOpenConfigUi;
this.Plugin.Interface.UiBuilder.Draw -= this.Draw;
}
private static bool IconButton(FontAwesomeIcon icon, string? id = null) {
@ -128,30 +129,31 @@ namespace ExpandedSearchInfo {
ImGui.End();
}
private void DrawExpandedSearchInfo() {
private unsafe void DrawExpandedSearchInfo() {
// check if the examine window is open
var addon = this.Plugin.Interface.Framework.Gui.GetAddonByName("CharacterInspect", 1);
if (addon is not {Visible: true}) {
var addonPtr = this.Plugin.GameGui.GetAddonByName("CharacterInspect", 1);
if (addonPtr == IntPtr.Zero) {
return;
}
var addon = (AtkUnitBase*) addonPtr;
if (addon->IsVisible) {
return;
}
// get examine window info
float width;
float height;
short x;
short y;
try {
width = addon.Width;
height = addon.Height;
x = addon.X;
y = addon.Y;
} catch (Exception) {
var rootNode = addon->RootNode;
if (rootNode == null) {
return;
}
var width = rootNode->Width * addon->Scale;
var height = rootNode->Height * addon->Scale;
var x = addon->X;
var y = addon->Y;
// check the last actor id recorded (should be who the examine window is showing)
var actorId = this.Plugin.Repository.LastActorId;
var actorId = this.Plugin.Repository.LastObjectId;
if (actorId == 0 || !this.Plugin.Repository.SearchInfos.TryGetValue(actorId, out var expanded)) {
return;
}

View File

@ -24,7 +24,7 @@ namespace ExpandedSearchInfo.Providers {
public abstract bool Matches(Uri uri);
public abstract IEnumerable<Uri>? ExtractUris(int actorId, string info);
public abstract IEnumerable<Uri>? ExtractUris(uint objectId, string info);
public abstract Task<ISearchInfoSection?> ExtractInfo(HttpResponseMessage response);

View File

@ -34,7 +34,7 @@ namespace ExpandedSearchInfo.Providers {
public override bool Matches(Uri uri) => Domains.Any(domain => uri.Host.EndsWith(domain));
public override IEnumerable<Uri>? ExtractUris(int actorId, string info) => null;
public override IEnumerable<Uri>? ExtractUris(uint objectId, string info) => null;
public override async Task<ISearchInfoSection?> ExtractInfo(HttpResponseMessage response) {
var document = await this.DownloadDocument(response);
@ -83,7 +83,7 @@ namespace ExpandedSearchInfo.Providers {
return new TextSection(
this,
$"{document.Title} (Carrd)",
response.RequestMessage.RequestUri,
response.RequestMessage!.RequestUri!,
text
);
}

View File

@ -27,19 +27,19 @@ namespace ExpandedSearchInfo.Providers {
public override void DrawConfig() {
}
public override bool Matches(Uri uri) => (uri.Host == "www.f-list.net" || uri.Host == "f-list.net") && uri.AbsolutePath.StartsWith("/c/");
public override bool Matches(Uri uri) => uri.Host is "www.f-list.net" or "f-list.net" && uri.AbsolutePath.StartsWith("/c/");
public override IEnumerable<Uri>? ExtractUris(int actorId, string info) {
public override IEnumerable<Uri>? ExtractUris(uint objectId, string info) {
if (!info.ToLowerInvariant().Contains("c/")) {
return null;
}
var actor = this.Plugin.Interface.ClientState.Actors.FirstOrDefault(actor => actor.ActorId == actorId);
if (actor == null) {
var obj = this.Plugin.ObjectTable.FirstOrDefault(obj => obj.ObjectId == objectId);
if (obj == null) {
return null;
}
var safeName = actor.Name.Replace("'", "");
var safeName = obj.Name.ToString().Replace("'", "");
return new[] {
new Uri($"https://www.f-list.net/c/{Uri.EscapeUriString(safeName)}"),
@ -99,7 +99,7 @@ namespace ExpandedSearchInfo.Providers {
return new FListSection(
this,
$"{charName} (F-List)",
response.RequestMessage.RequestUri,
response.RequestMessage!.RequestUri!,
info,
stats,
fave,

View File

@ -34,10 +34,10 @@ namespace ExpandedSearchInfo.Providers {
/// For providers that require Uris, this can return null.
/// For providers that don't require Uris, this must return a Uri extracted from the given search info.
/// </summary>
/// <param name="actorId">The actor ID associated with the search info</param>
/// <param name="objectId">The actor ID associated with the search info</param>
/// <param name="info">A character's full search info</param>
/// <returns>null for providers that require Uris, a Uri for providers that don't</returns>
IEnumerable<Uri>? ExtractUris(int actorId, string info);
IEnumerable<Uri>? ExtractUris(uint objectId, string info);
/// <summary>
/// Extract the search info to be displayed given the HTTP response from a Uri.

View File

@ -30,7 +30,7 @@ namespace ExpandedSearchInfo.Providers {
public bool Matches(Uri uri) => uri.Host == "pastebin.com" && uri.AbsolutePath.Length > 1;
public IEnumerable<Uri>? ExtractUris(int actorId, string info) {
public IEnumerable<Uri>? ExtractUris(uint objectId, string info) {
var matches = Matcher.Matches(info);
return matches.Count == 0
? null
@ -38,11 +38,11 @@ namespace ExpandedSearchInfo.Providers {
}
public async Task<ISearchInfoSection?> ExtractInfo(HttpResponseMessage response) {
if (response.Content.Headers.ContentType.MediaType != "text/plain") {
if (response.Content.Headers.ContentType?.MediaType != "text/plain") {
return null;
}
var id = response.RequestMessage.RequestUri.AbsolutePath.Split('/').LastOrDefault();
var id = response.RequestMessage!.RequestUri!.AbsolutePath.Split('/').LastOrDefault();
var info = await response.Content.ReadAsStringAsync();

View File

@ -26,17 +26,17 @@ namespace ExpandedSearchInfo.Providers {
public bool Matches(Uri uri) => true;
public IEnumerable<Uri>? ExtractUris(int actorId, string info) => null;
public IEnumerable<Uri>? ExtractUris(uint objectId, string info) => null;
public async Task<ISearchInfoSection?> ExtractInfo(HttpResponseMessage response) {
if (response.Content.Headers.ContentType.MediaType != "text/plain") {
if (response.Content.Headers.ContentType?.MediaType != "text/plain") {
return null;
}
var info = await response.Content.ReadAsStringAsync();
var uri = response.RequestMessage.RequestUri;
return new TextSection(this, $"Text##{uri}", response.RequestMessage.RequestUri, info);
var uri = response.RequestMessage!.RequestUri!;
return new TextSection(this, $"Text##{uri}", uri, info);
}
}
}

View File

@ -29,9 +29,9 @@ namespace ExpandedSearchInfo.Providers {
public override void DrawConfig() {
}
public override bool Matches(Uri uri) => uri.Host == "refsheet.net" || uri.Host == "ref.st";
public override bool Matches(Uri uri) => uri.Host is "refsheet.net" or "ref.st";
public override IEnumerable<Uri>? ExtractUris(int actorId, string info) => null;
public override IEnumerable<Uri>? ExtractUris(uint objectId, string info) => null;
public override async Task<ISearchInfoSection?> ExtractInfo(HttpResponseMessage response) {
var document = await this.DownloadDocument(response);
@ -52,6 +52,10 @@ namespace ExpandedSearchInfo.Providers {
var json = jsonLine.Substring(JsonLineStart.Length, jsonLine.Length - JsonLineStart.Length - 1);
var parsed = JsonConvert.DeserializeObject<RefsheetData>(json);
if (parsed == null) {
return null;
}
var character = parsed.EagerLoad.Character;
// get character name
@ -113,7 +117,7 @@ namespace ExpandedSearchInfo.Providers {
return new RefsheetSection(
this,
$"{name} (Refsheet)",
response.RequestMessage.RequestUri,
response.RequestMessage!.RequestUri!,
attributes,
notes,
cards

View File

@ -4,10 +4,9 @@ using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Plugin;
using Dalamud.Logging;
using ExpandedSearchInfo.Providers;
using ExpandedSearchInfo.Sections;
using Nager.PublicSuffix;
@ -26,8 +25,8 @@ namespace ExpandedSearchInfo {
public class SearchInfoRepository : IDisposable {
private Plugin Plugin { get; }
private DomainParser Parser { get; }
internal ConcurrentDictionary<int, ExpandedSearchInfo> SearchInfos { get; } = new();
internal int LastActorId { get; private set; }
internal ConcurrentDictionary<uint, ExpandedSearchInfo> SearchInfos { get; } = new();
internal uint LastObjectId { get; private set; }
private List<IProvider> Providers { get; } = new();
internal IEnumerable<IProvider> AllProviders => this.Providers;
@ -62,48 +61,48 @@ namespace ExpandedSearchInfo {
this.Providers.Add(new PlainTextProvider(this.Plugin));
}
private void ProcessSearchInfo(int actorId, SeString raw) {
this.LastActorId = actorId;
private void ProcessSearchInfo(uint objectId, SeString raw) {
this.LastObjectId = objectId;
var info = raw.TextValue;
// if empty search info, short circuit
if (string.IsNullOrWhiteSpace(info)) {
// remove any existing search info
this.SearchInfos.TryRemove(actorId, out _);
this.SearchInfos.TryRemove(objectId, out _);
return;
}
// check to see if info has changed
#if RELEASE
if (this.SearchInfos.TryGetValue(actorId, out var existing)) {
if (this.SearchInfos.TryGetValue(objectId, out var existing)) {
if (existing.Info == info) {
return;
}
}
#endif
new Thread(async () => {
Task.Run(async () => {
try {
await this.DoExtraction(actorId, info);
await this.DoExtraction(objectId, info);
} catch (Exception ex) {
PluginLog.LogError($"Error in extraction thread:\n{ex}");
}
}).Start();
});
}
private async Task DoExtraction(int actorId, string info) {
private async Task DoExtraction(uint objectId, string info) {
var downloadUris = new List<Uri>();
// extract uris from the search info with providers
var extractedUris = this.Providers
.Where(provider => provider.Config.Enabled && provider.ExtractsUris)
.Select(provider => provider.ExtractUris(actorId, info))
.Select(provider => provider.ExtractUris(objectId, info))
.Where(uris => uris != null)
.SelectMany(uris => uris);
.SelectMany(uris => uris!);
// add the extracted uris to the list
downloadUris.AddRange(extractedUris!);
downloadUris.AddRange(extractedUris);
// go word-by-word and try to parse a uri
foreach (var word in info.Split(' ', '\n', '\r')) {
@ -128,15 +127,15 @@ namespace ExpandedSearchInfo {
// if there were no uris found or extracted, remove existing search info and stop
if (downloadUris.Count == 0) {
this.SearchInfos.TryRemove(actorId, out _);
this.SearchInfos.TryRemove(objectId, out _);
return;
}
// do the downloads
await this.DownloadAndExtract(actorId, info, downloadUris);
await this.DownloadAndExtract(objectId, info, downloadUris);
}
private async Task DownloadAndExtract(int actorId, string info, IEnumerable<Uri> uris) {
private async Task DownloadAndExtract(uint objectId, string info, IEnumerable<Uri> uris) {
var handler = new HttpClientHandler {
UseCookies = true,
AllowAutoRedirect = true,
@ -183,12 +182,12 @@ namespace ExpandedSearchInfo {
// remove expanded search info if no sections resulted
if (sections.Count == 0) {
this.SearchInfos.TryRemove(actorId, out _);
this.SearchInfos.TryRemove(objectId, out _);
return;
}
// otherwise set the expanded search info for this actor id
this.SearchInfos[actorId] = new ExpandedSearchInfo(info, sections);
this.SearchInfos[objectId] = new ExpandedSearchInfo(info, sections);
}
}
}

BIN
icon.png Normal file

Binary file not shown.

76
icon.svg Executable file
View File

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
x="0px"
y="0px"
viewBox="0 0 98.199997 98.199997"
enable-background="new 0 0 100 100"
xml:space="preserve"
id="svg8"
sodipodi:docname="icon.svg"
width="98.199997"
height="98.199997"
inkscape:version="1.1 (c68e22c387, 2021-05-23)"
inkscape:export-filename="D:\code\ExpandedSearchInfo\icon.png"
inkscape:export-xdpi="500.53"
inkscape:export-ydpi="500.53"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs12" /><sodipodi:namedview
id="namedview10"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
showgrid="false"
inkscape:zoom="6.656"
inkscape:cx="49.053486"
inkscape:cy="49.654447"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="1592"
inkscape:window-y="32"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" /><g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
transform="translate(-0.9,-0.9)"><path
d="M 28.3,50 C 28.3,38 38,28.3 50,28.3 62,28.3 71.7,38 71.7,50 71.7,62 62,71.7 50,71.7 38,71.7 28.3,62 28.3,50 Z"
id="path1024"
style="fill:#1720a9;fill-opacity:1" /></g><g
inkscape:groupmode="layer"
id="layer1"
inkscape:label="Layer 1"
transform="translate(-0.9,-0.9)"><path
d="m 35.7,50 c 0,4.2 1.8,7.9 4.6,10.5 0.4,-3.6 2.9,-6.7 6.2,-8 -1.9,-1.2 -3.1,-3.3 -3.1,-5.6 0,-3.7 3,-6.7 6.7,-6.7 3.7,0 6.7,3 6.7,6.7 0,2.4 -1.2,4.5 -3.1,5.6 3.3,1.3 5.7,4.3 6.2,8 2.8,-2.6 4.6,-6.3 4.6,-10.5 0,-7.9 -6.4,-14.3 -14.3,-14.3 -8.1,0 -14.5,6.4 -14.5,14.3 z"
id="path1026"
style="fill:#ffffff;fill-opacity:1" /><path
d="m 50,77.8 c -1.5,0 -3.1,-0.2 -4.5,-0.4 v 9.2 h -4.8 c -0.5,0 -0.8,0.6 -0.5,1 l 9.2,11.2 c 0.3,0.4 0.9,0.4 1.2,0 l 9.2,-11.2 c 0.3,-0.4 0,-1 -0.5,-1 h -4.8 v -9.2 c -1.4,0.2 -3,0.4 -4.5,0.4 z"
id="path1022"
style="fill:#ffffff;fill-opacity:1" /><path
d="m 30.7,82.5 -3.4,-3.4 6.5,-6.5 c -2.5,-1.8 -4.6,-3.9 -6.4,-6.4 l -6.5,6.5 -3.4,-3.4 c -0.4,-0.4 -1,-0.1 -1,0.4 l -1.4,14.4 c 0,0.5 0.4,0.9 0.8,0.8 l 14.4,-1.4 c 0.5,-0.1 0.8,-0.7 0.4,-1 z"
id="path1020"
style="fill:#ffffff;fill-opacity:1" /><path
d="m 77.8,50 c 0,1.5 -0.2,3 -0.4,4.5 h 9.2 v 4.8 c 0,0.5 0.6,0.8 1,0.5 l 11.2,-9.2 c 0.4,-0.3 0.4,-0.9 0,-1.2 L 87.6,40.2 c -0.4,-0.3 -1,0 -1,0.5 v 4.8 h -9.2 c 0.2,1.5 0.4,3 0.4,4.5 z"
id="path1018"
style="fill:#ffffff;fill-opacity:1" /><path
d="m 22.2,50 c 0,-1.5 0.2,-3 0.4,-4.5 h -9.2 v -4.8 c 0,-0.5 -0.6,-0.8 -1,-0.5 L 1.2,49.4 c -0.4,0.3 -0.4,0.9 0,1.2 l 11.2,9.2 c 0.4,0.3 1,0 1,-0.5 v -4.8 h 9.2 C 22.4,53 22.2,51.5 22.2,50 Z"
id="path1016"
style="fill:#ffffff;fill-opacity:1" /><path
d="m 69.7,83.5 14.4,1.4 c 0.5,0 0.9,-0.4 0.8,-0.8 L 83.5,69.7 c -0.1,-0.5 -0.7,-0.7 -1,-0.4 l -3.4,3.4 -6.5,-6.5 c -1.8,2.5 -3.9,4.6 -6.4,6.4 l 6.5,6.5 -3.4,3.4 c -0.4,0.3 -0.1,0.9 0.4,1 z"
id="path1014"
style="fill:#ffffff;fill-opacity:1" /><path
d="m 72.5,33.9 6.5,-6.5 3.4,3.4 c 0.4,0.4 1,0.1 1,-0.4 L 84.8,16 c 0,-0.5 -0.4,-0.9 -0.8,-0.8 l -14.4,1.4 c -0.5,0.1 -0.7,0.7 -0.4,1 l 3.4,3.4 -6.5,6.5 c 2.5,1.7 4.7,3.9 6.4,6.4 z"
id="path1012"
style="fill:#ffffff;fill-opacity:1" /><path
d="m 17.5,30.7 3.4,-3.4 6.5,6.5 c 1.8,-2.5 3.9,-4.6 6.4,-6.4 l -6.5,-6.5 3.4,-3.4 c 0.4,-0.4 0.1,-1 -0.4,-1 L 15.9,15.1 c -0.5,0 -0.9,0.4 -0.8,0.8 l 1.4,14.4 c 0.1,0.5 0.7,0.8 1,0.4 z"
id="path1010"
style="fill:#ffffff;fill-opacity:1" /><path
d="M 40.2,12.4 49.4,1.2 c 0.3,-0.4 0.9,-0.4 1.2,0 l 9.2,11.2 c 0.3,0.4 0,1 -0.5,1 h -4.8 v 9.2 c -1.5,-0.2 -3,-0.4 -4.5,-0.4 -1.5,0 -3.1,0.2 -4.5,0.4 v -9.2 h -4.8 c -0.5,0 -0.8,-0.6 -0.5,-1 z"
id="path2"
style="fill:#ffffff;fill-opacity:1" /></g></svg>

After

Width:  |  Height:  |  Size: 4.1 KiB