Added dithering for images on packing, added deleting incomplete bin if error happened on packing, closes #2
parent
c5a1ed3fef
commit
a6f36424e8
|
@ -0,0 +1,45 @@
|
||||||
|
using System.Drawing;
|
||||||
|
|
||||||
|
namespace Resources.Dithering
|
||||||
|
{
|
||||||
|
public class ColorError
|
||||||
|
{
|
||||||
|
public const int LowLevel = 0x00;
|
||||||
|
public const int HighLevel = 0xff;
|
||||||
|
public const int Treshold = 0x80;
|
||||||
|
|
||||||
|
public ColorError(Color original)
|
||||||
|
{
|
||||||
|
var r = original.R < Treshold ? LowLevel : HighLevel;
|
||||||
|
var g = original.G < Treshold ? LowLevel : HighLevel;
|
||||||
|
var b = original.B < Treshold ? LowLevel : HighLevel;
|
||||||
|
|
||||||
|
NewColor = Color.FromArgb(original.A, r, g, b);
|
||||||
|
|
||||||
|
ErrorR = original.R - r;
|
||||||
|
ErrorG = original.G - g;
|
||||||
|
ErrorB = original.B - b;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Color NewColor { get; }
|
||||||
|
public int ErrorR { get; }
|
||||||
|
public int ErrorG { get; }
|
||||||
|
public int ErrorB { get; }
|
||||||
|
public bool IsZero => ErrorR == 0 && ErrorG == 0 && ErrorB == 0;
|
||||||
|
|
||||||
|
public Color ApplyError(Color color, int part, int total)
|
||||||
|
{
|
||||||
|
return Color.FromArgb(
|
||||||
|
NewColor.A,
|
||||||
|
CheckBounds(color.R + ErrorR * part / total),
|
||||||
|
CheckBounds(color.G + ErrorG * part / total),
|
||||||
|
CheckBounds(color.B + ErrorB * part / total)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte CheckBounds(int value)
|
||||||
|
{
|
||||||
|
return (byte) (value < 0 ? 0 : (value > 255 ? 255 : value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
using System.Drawing;
|
||||||
|
using NLog;
|
||||||
|
|
||||||
|
namespace Resources.Dithering
|
||||||
|
{
|
||||||
|
public class FloydSteinbergDitherer
|
||||||
|
{
|
||||||
|
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
|
||||||
|
|
||||||
|
public static void Process(Bitmap image)
|
||||||
|
{
|
||||||
|
var isImageAltered = false;
|
||||||
|
for (var y = 0; y < image.Height; y++)
|
||||||
|
for (var x = 0; x < image.Width; x++)
|
||||||
|
{
|
||||||
|
var color = image.GetPixel(x, y);
|
||||||
|
var colorError = new ColorError(color);
|
||||||
|
|
||||||
|
if (colorError.IsZero) continue;
|
||||||
|
|
||||||
|
if (!isImageAltered)
|
||||||
|
{
|
||||||
|
Logger.Warn(
|
||||||
|
"Dithering applied for an image. Resource in watch face will use only supported colors. You can't get back original image by unpacking watch face."
|
||||||
|
);
|
||||||
|
isImageAltered = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
image.SetPixel(x, y, colorError.NewColor);
|
||||||
|
|
||||||
|
if (x + 1 < image.Width)
|
||||||
|
{
|
||||||
|
color = image.GetPixel(x + 1, y);
|
||||||
|
image.SetPixel(x + 1, y, colorError.ApplyError(color, 7, 16));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (y + 1 < image.Height)
|
||||||
|
{
|
||||||
|
if (x > 1)
|
||||||
|
{
|
||||||
|
color = image.GetPixel(x - 1, y + 1);
|
||||||
|
image.SetPixel(x - 1, y + 1, colorError.ApplyError(color, 3, 16));
|
||||||
|
}
|
||||||
|
|
||||||
|
color = image.GetPixel(x, y + 1);
|
||||||
|
image.SetPixel(x, y + 1, colorError.ApplyError(color, 5, 16));
|
||||||
|
|
||||||
|
if (x < image.Width - 1)
|
||||||
|
{
|
||||||
|
color = image.GetPixel(x + 1, y + 1);
|
||||||
|
image.SetPixel(x + 1, y + 1, colorError.ApplyError(color, 1, 16));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -70,7 +70,8 @@ namespace Resources
|
||||||
else
|
else
|
||||||
Logger.Warn("Palette item {0}: R {1:X2}, G {2:X2}, B {3:X2}, color isn't supported!", i, r, g, b);
|
Logger.Warn("Palette item {0}: R {1:X2}, G {2:X2}, B {3:X2}, color isn't supported!", i, r, g, b);
|
||||||
|
|
||||||
_palette[i] = Color.FromArgb(_transparency && i == 0 ? 0x00 : 0xff, r, g, b);
|
var alpha = _transparency && i == 0 ? 0x00 : 0xff;
|
||||||
|
_palette[i] = Color.FromArgb(alpha, r, g, b);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,62 +1,72 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
|
using System.Drawing.Imaging;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using NLog;
|
using NLog;
|
||||||
|
using Resources.Dithering;
|
||||||
|
|
||||||
namespace Resources
|
namespace Resources
|
||||||
{
|
{
|
||||||
public class ImageWriter
|
public class ImageWriter
|
||||||
{
|
{
|
||||||
private const int MaxSupportedColors = 9;
|
|
||||||
private static readonly byte[] Signature = {(byte) 'B', (byte) 'M', (byte) 'd', 0};
|
|
||||||
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
|
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
|
||||||
private readonly Bitmap _image;
|
private static readonly byte[] Signature = {(byte) 'B', (byte) 'M', (byte) 'd', 0};
|
||||||
private readonly List<Color> _palette;
|
private readonly List<Color> _palette;
|
||||||
|
|
||||||
private readonly BinaryWriter _writer;
|
private readonly BinaryWriter _writer;
|
||||||
|
|
||||||
private ushort _bitsPerPixel;
|
private ushort _bitsPerPixel;
|
||||||
private ushort _height;
|
private ushort _height;
|
||||||
|
private Bitmap _image;
|
||||||
private ushort _paletteColors;
|
private ushort _paletteColors;
|
||||||
private ushort _rowLengthInBytes;
|
private ushort _rowLengthInBytes;
|
||||||
private ushort _transparency;
|
private ushort _transparency;
|
||||||
private ushort _width;
|
private ushort _width;
|
||||||
|
|
||||||
public ImageWriter(Stream stream, Bitmap image)
|
public ImageWriter(Stream stream)
|
||||||
{
|
{
|
||||||
_writer = new BinaryWriter(stream);
|
_writer = new BinaryWriter(stream);
|
||||||
_image = image;
|
|
||||||
_palette = new List<Color>();
|
_palette = new List<Color>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Write()
|
public void Write(Bitmap image)
|
||||||
{
|
{
|
||||||
_writer.Write(Signature);
|
_image = image;
|
||||||
|
_width = (ushort) image.Width;
|
||||||
|
_height = (ushort) image.Height;
|
||||||
|
|
||||||
_width = (ushort) _image.Width;
|
ApplyDithering();
|
||||||
_height = (ushort) _image.Height;
|
|
||||||
ExtractPalette();
|
ExtractPalette();
|
||||||
|
|
||||||
_paletteColors = (ushort) _palette.Count;
|
|
||||||
_bitsPerPixel = (ushort) Math.Ceiling(Math.Log(_paletteColors, 2));
|
|
||||||
if (_bitsPerPixel == 3) _bitsPerPixel = 4;
|
if (_bitsPerPixel == 3) _bitsPerPixel = 4;
|
||||||
if (_bitsPerPixel == 0) _bitsPerPixel = 1;
|
if (_bitsPerPixel == 0) _bitsPerPixel = 1;
|
||||||
|
|
||||||
if (_bitsPerPixel != 1 && _bitsPerPixel != 2 && _bitsPerPixel != 4)
|
if (_bitsPerPixel > 4)
|
||||||
throw new ArgumentException($"{0} bits per pixel doesn't supported.");
|
throw new ArgumentException(
|
||||||
|
$"The image has {_bitsPerPixel} bit/pixel and can't be packed used on the watches. Looks like dithering works wincorrectly on the image."
|
||||||
|
);
|
||||||
|
|
||||||
_rowLengthInBytes = (ushort) Math.Ceiling(_width * _bitsPerPixel / 8.0);
|
_rowLengthInBytes = (ushort) Math.Ceiling(_width * _bitsPerPixel / 8.0);
|
||||||
|
|
||||||
if (_paletteColors > MaxSupportedColors)
|
_writer.Write(Signature);
|
||||||
Logger.Warn($"Image cotains {0} colors but biggest known supported values is {1} colors",
|
|
||||||
_paletteColors, MaxSupportedColors);
|
|
||||||
|
|
||||||
WriteHeader();
|
WriteHeader();
|
||||||
WritePalette();
|
WritePalette();
|
||||||
WriteImage();
|
WriteImage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ApplyDithering()
|
||||||
|
{
|
||||||
|
var clone = new Bitmap(_image.Width, _image.Height, PixelFormat.Format32bppArgb);
|
||||||
|
using (var gr = Graphics.FromImage(clone))
|
||||||
|
{
|
||||||
|
gr.DrawImage(_image, new Rectangle(0, 0, clone.Width, clone.Height));
|
||||||
|
}
|
||||||
|
FloydSteinbergDitherer.Process(clone);
|
||||||
|
_image = clone;
|
||||||
|
}
|
||||||
|
|
||||||
private void ExtractPalette()
|
private void ExtractPalette()
|
||||||
{
|
{
|
||||||
Logger.Trace("Extracting palette...");
|
Logger.Trace("Extracting palette...");
|
||||||
|
@ -66,7 +76,7 @@ namespace Resources
|
||||||
var color = _image.GetPixel(x, y);
|
var color = _image.GetPixel(x, y);
|
||||||
if (_palette.Contains(color)) continue;
|
if (_palette.Contains(color)) continue;
|
||||||
|
|
||||||
if (color.A == 0 && _transparency == 0)
|
if (color.A < 0x80 && _transparency == 0)
|
||||||
{
|
{
|
||||||
Logger.Trace("Palette item {0}: R {1:X2}, G {2:X2}, B {3:X2}, Transaparent color",
|
Logger.Trace("Palette item {0}: R {1:X2}, G {2:X2}, B {3:X2}, Transaparent color",
|
||||||
_palette.Count, color.R, color.G, color.B
|
_palette.Count, color.R, color.G, color.B
|
||||||
|
@ -82,6 +92,8 @@ namespace Resources
|
||||||
_palette.Add(color);
|
_palette.Add(color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_paletteColors = (ushort) _palette.Count;
|
||||||
|
_bitsPerPixel = (ushort) Math.Ceiling(Math.Log(_paletteColors, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void WriteHeader()
|
private void WriteHeader()
|
||||||
|
@ -132,8 +144,15 @@ namespace Resources
|
||||||
for (var x = 0; x < _width; x++)
|
for (var x = 0; x < _width; x++)
|
||||||
{
|
{
|
||||||
var color = _image.GetPixel(x, y);
|
var color = _image.GetPixel(x, y);
|
||||||
var paletteIndex = paletteHash[color];
|
if (color.A < 0x80 && _transparency == 1)
|
||||||
bitWriter.WriteBits(paletteIndex, _bitsPerPixel);
|
{
|
||||||
|
bitWriter.WriteBits(0, _bitsPerPixel);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var paletteIndex = paletteHash[color];
|
||||||
|
bitWriter.WriteBits(paletteIndex, _bitsPerPixel);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
bitWriter.Flush();
|
bitWriter.Flush();
|
||||||
_writer.Write(rowData);
|
_writer.Write(rowData);
|
||||||
|
|
|
@ -31,5 +31,5 @@ using System.Runtime.InteropServices;
|
||||||
// You can specify all the values or you can default the Build and Revision Numbers
|
// You can specify all the values or you can default the Build and Revision Numbers
|
||||||
// by using the '*' as shown below:
|
// by using the '*' as shown below:
|
||||||
// [assembly: AssemblyVersion("1.0.*")]
|
// [assembly: AssemblyVersion("1.0.*")]
|
||||||
[assembly: AssemblyVersion("1.0.0.0")]
|
[assembly: AssemblyVersion("1.0.0.2")]
|
||||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
[assembly: AssemblyFileVersion("1.0.0.2")]
|
|
@ -46,6 +46,8 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="BitReader.cs" />
|
<Compile Include="BitReader.cs" />
|
||||||
<Compile Include="BitWriter.cs" />
|
<Compile Include="BitWriter.cs" />
|
||||||
|
<Compile Include="Dithering\ColorError.cs" />
|
||||||
|
<Compile Include="Dithering\FloydSteinbergDitherer.cs" />
|
||||||
<Compile Include="ImageWriter.cs" />
|
<Compile Include="ImageWriter.cs" />
|
||||||
<Compile Include="ResourcesExtractor.cs" />
|
<Compile Include="ResourcesExtractor.cs" />
|
||||||
<Compile Include="ResourcesHeader.cs" />
|
<Compile Include="ResourcesHeader.cs" />
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
<packages>
|
<packages>
|
||||||
<package id="NLog" version="4.4.12" targetFramework="net40-client" />
|
<package id="NLog" version="4.4.12" targetFramework="net40-client" />
|
||||||
</packages>
|
</packages>
|
|
@ -10,7 +10,7 @@ namespace WatchFace.Parser.Elements.StatusElements
|
||||||
|
|
||||||
[ParameterId(2)]
|
[ParameterId(2)]
|
||||||
[ParameterImageIndex]
|
[ParameterImageIndex]
|
||||||
public long ImageIndexOn { get; set; }
|
public long? ImageIndexOn { get; set; }
|
||||||
|
|
||||||
[ParameterId(3)]
|
[ParameterId(3)]
|
||||||
[ParameterImageIndex]
|
[ParameterImageIndex]
|
||||||
|
|
|
@ -18,7 +18,7 @@ namespace WatchFace.Parser.JsonConverters
|
||||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue,
|
public override object ReadJson(JsonReader reader, Type objectType, object existingValue,
|
||||||
JsonSerializer serializer)
|
JsonSerializer serializer)
|
||||||
{
|
{
|
||||||
var str = (string)reader.Value;
|
var str = (string) reader.Value;
|
||||||
if (str == null || !str.StartsWith("0x"))
|
if (str == null || !str.StartsWith("0x"))
|
||||||
throw new JsonSerializationException();
|
throw new JsonSerializationException();
|
||||||
return Convert.ToInt64(str.Substring(2), 16);
|
return Convert.ToInt64(str.Substring(2), 16);
|
||||||
|
|
|
@ -31,5 +31,5 @@ using System.Runtime.InteropServices;
|
||||||
// You can specify all the values or you can default the Build and Revision Numbers
|
// You can specify all the values or you can default the Build and Revision Numbers
|
||||||
// by using the '*' as shown below:
|
// by using the '*' as shown below:
|
||||||
// [assembly: AssemblyVersion("1.0.*")]
|
// [assembly: AssemblyVersion("1.0.*")]
|
||||||
[assembly: AssemblyVersion("1.0.0.1")]
|
[assembly: AssemblyVersion("1.0.0.2")]
|
||||||
[assembly: AssemblyFileVersion("1.0.0.1")]
|
[assembly: AssemblyFileVersion("1.0.0.2")]
|
|
@ -2,8 +2,6 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
|
||||||
using NLog;
|
using NLog;
|
||||||
using WatchFace.Parser.Attributes;
|
using WatchFace.Parser.Attributes;
|
||||||
|
|
||||||
|
@ -42,8 +40,10 @@ namespace WatchFace.Parser.Utils
|
||||||
var propertyType = propertyInfo.PropertyType;
|
var propertyType = propertyInfo.PropertyType;
|
||||||
dynamic propertyValue = propertyInfo.GetValue(serializable, null);
|
dynamic propertyValue = propertyInfo.GetValue(serializable, null);
|
||||||
|
|
||||||
var imageIndexAttribute =ElementsHelper.GetCustomAttributeFor<ParameterImageIndexAttribute>(propertyInfo);
|
var imageIndexAttribute =
|
||||||
var imagesCountAttribute =ElementsHelper.GetCustomAttributeFor<ParameterImagesCountAttribute>(propertyInfo);
|
ElementsHelper.GetCustomAttributeFor<ParameterImageIndexAttribute>(propertyInfo);
|
||||||
|
var imagesCountAttribute =
|
||||||
|
ElementsHelper.GetCustomAttributeFor<ParameterImagesCountAttribute>(propertyInfo);
|
||||||
|
|
||||||
if (imagesCountAttribute != null && imageIndexAttribute != null)
|
if (imagesCountAttribute != null && imageIndexAttribute != null)
|
||||||
throw new ArgumentException(
|
throw new ArgumentException(
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using NLog;
|
using NLog;
|
||||||
using WatchFace.Parser.Attributes;
|
|
||||||
using WatchFace.Parser.Models;
|
using WatchFace.Parser.Models;
|
||||||
|
|
||||||
namespace WatchFace.Parser.Utils
|
namespace WatchFace.Parser.Utils
|
||||||
|
|
|
@ -90,7 +90,7 @@ namespace WatchFace.Parser
|
||||||
|
|
||||||
var encodedImage = new MemoryStream();
|
var encodedImage = new MemoryStream();
|
||||||
Logger.Debug("Writing image {0}...", i);
|
Logger.Debug("Writing image {0}...", i);
|
||||||
new ImageWriter(encodedImage, _images[i]).Write();
|
new ImageWriter(encodedImage).Write(_images[i]);
|
||||||
offset += (uint) encodedImage.Length;
|
offset += (uint) encodedImage.Length;
|
||||||
encodedImages[i] = encodedImage;
|
encodedImages[i] = encodedImage;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
<packages>
|
<packages>
|
||||||
<package id="Newtonsoft.Json" version="10.0.3" targetFramework="net40-client" />
|
<package id="Newtonsoft.Json" version="10.0.3" targetFramework="net40-client" />
|
||||||
<package id="NLog" version="4.4.12" targetFramework="net40-client" />
|
<package id="NLog" version="4.4.12" targetFramework="net40-client" />
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
<configuration>
|
<configuration>
|
||||||
<startup>
|
<startup>
|
||||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0,Profile=Client"/>
|
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0,Profile=Client" />
|
||||||
</startup>
|
</startup>
|
||||||
</configuration>
|
</configuration>
|
|
@ -21,7 +21,8 @@ namespace WatchFace
|
||||||
{
|
{
|
||||||
if (args.Length == 0 || args[0] == null)
|
if (args.Length == 0 || args[0] == null)
|
||||||
{
|
{
|
||||||
Console.WriteLine("{0}.exe unpacks and packs Amazfit Bip downloadable watch faces and unpacks res files.", AppName);
|
Console.WriteLine(
|
||||||
|
"{0}.exe unpacks and packs Amazfit Bip downloadable watch faces and unpacks res files.", AppName);
|
||||||
Console.WriteLine();
|
Console.WriteLine();
|
||||||
Console.WriteLine("Usage examples:");
|
Console.WriteLine("Usage examples:");
|
||||||
Console.WriteLine(" {0}.exe watchface.bin - unpacks watchface images and config", AppName);
|
Console.WriteLine(" {0}.exe watchface.bin - unpacks watchface images and config", AppName);
|
||||||
|
@ -64,7 +65,7 @@ namespace WatchFace
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Logger.Fatal(e.Message);
|
Logger.Fatal(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -80,7 +81,15 @@ namespace WatchFace
|
||||||
if (watchFace == null) return;
|
if (watchFace == null) return;
|
||||||
|
|
||||||
var imagesDirectory = Path.GetDirectoryName(inputFileName);
|
var imagesDirectory = Path.GetDirectoryName(inputFileName);
|
||||||
WriteWatchFace(outputFileName, imagesDirectory, watchFace);
|
try
|
||||||
|
{
|
||||||
|
WriteWatchFace(outputFileName, imagesDirectory, watchFace);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
File.Delete(outputFileName);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void UnpackWatchFace(string inputFileName)
|
private static void UnpackWatchFace(string inputFileName)
|
||||||
|
|
|
@ -31,5 +31,5 @@ using System.Runtime.InteropServices;
|
||||||
// You can specify all the values or you can default the Build and Revision Numbers
|
// You can specify all the values or you can default the Build and Revision Numbers
|
||||||
// by using the '*' as shown below:
|
// by using the '*' as shown below:
|
||||||
// [assembly: AssemblyVersion("1.0.*")]
|
// [assembly: AssemblyVersion("1.0.*")]
|
||||||
[assembly: AssemblyVersion("1.0.0.0")]
|
[assembly: AssemblyVersion("1.0.0.2")]
|
||||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
[assembly: AssemblyFileVersion("1.0.0.2")]
|
|
@ -1,4 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
<packages>
|
<packages>
|
||||||
<package id="Newtonsoft.Json" version="10.0.3" targetFramework="net40-client" />
|
<package id="Newtonsoft.Json" version="10.0.3" targetFramework="net40-client" />
|
||||||
<package id="NLog" version="4.4.12" targetFramework="net40-client" />
|
<package id="NLog" version="4.4.12" targetFramework="net40-client" />
|
||||||
|
|
Loading…
Reference in New Issue