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
|
||||
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.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using NLog;
|
||||
using Resources.Dithering;
|
||||
|
||||
namespace Resources
|
||||
{
|
||||
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 readonly Bitmap _image;
|
||||
private static readonly byte[] Signature = {(byte) 'B', (byte) 'M', (byte) 'd', 0};
|
||||
private readonly List<Color> _palette;
|
||||
|
||||
private readonly BinaryWriter _writer;
|
||||
|
||||
private ushort _bitsPerPixel;
|
||||
private ushort _height;
|
||||
private Bitmap _image;
|
||||
private ushort _paletteColors;
|
||||
private ushort _rowLengthInBytes;
|
||||
private ushort _transparency;
|
||||
private ushort _width;
|
||||
|
||||
public ImageWriter(Stream stream, Bitmap image)
|
||||
public ImageWriter(Stream stream)
|
||||
{
|
||||
_writer = new BinaryWriter(stream);
|
||||
_image = image;
|
||||
_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;
|
||||
_height = (ushort) _image.Height;
|
||||
ApplyDithering();
|
||||
ExtractPalette();
|
||||
|
||||
_paletteColors = (ushort) _palette.Count;
|
||||
_bitsPerPixel = (ushort) Math.Ceiling(Math.Log(_paletteColors, 2));
|
||||
if (_bitsPerPixel == 3) _bitsPerPixel = 4;
|
||||
if (_bitsPerPixel == 0) _bitsPerPixel = 1;
|
||||
|
||||
if (_bitsPerPixel != 1 && _bitsPerPixel != 2 && _bitsPerPixel != 4)
|
||||
throw new ArgumentException($"{0} bits per pixel doesn't supported.");
|
||||
if (_bitsPerPixel > 4)
|
||||
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);
|
||||
|
||||
if (_paletteColors > MaxSupportedColors)
|
||||
Logger.Warn($"Image cotains {0} colors but biggest known supported values is {1} colors",
|
||||
_paletteColors, MaxSupportedColors);
|
||||
_writer.Write(Signature);
|
||||
|
||||
WriteHeader();
|
||||
WritePalette();
|
||||
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()
|
||||
{
|
||||
Logger.Trace("Extracting palette...");
|
||||
|
@ -66,7 +76,7 @@ namespace Resources
|
|||
var color = _image.GetPixel(x, y);
|
||||
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",
|
||||
_palette.Count, color.R, color.G, color.B
|
||||
|
@ -82,6 +92,8 @@ namespace Resources
|
|||
_palette.Add(color);
|
||||
}
|
||||
}
|
||||
_paletteColors = (ushort) _palette.Count;
|
||||
_bitsPerPixel = (ushort) Math.Ceiling(Math.Log(_paletteColors, 2));
|
||||
}
|
||||
|
||||
private void WriteHeader()
|
||||
|
@ -132,8 +144,15 @@ namespace Resources
|
|||
for (var x = 0; x < _width; x++)
|
||||
{
|
||||
var color = _image.GetPixel(x, y);
|
||||
var paletteIndex = paletteHash[color];
|
||||
bitWriter.WriteBits(paletteIndex, _bitsPerPixel);
|
||||
if (color.A < 0x80 && _transparency == 1)
|
||||
{
|
||||
bitWriter.WriteBits(0, _bitsPerPixel);
|
||||
}
|
||||
else
|
||||
{
|
||||
var paletteIndex = paletteHash[color];
|
||||
bitWriter.WriteBits(paletteIndex, _bitsPerPixel);
|
||||
}
|
||||
}
|
||||
bitWriter.Flush();
|
||||
_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
|
||||
// by using the '*' as shown below:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||
[assembly: AssemblyVersion("1.0.0.2")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.2")]
|
|
@ -46,6 +46,8 @@
|
|||
<ItemGroup>
|
||||
<Compile Include="BitReader.cs" />
|
||||
<Compile Include="BitWriter.cs" />
|
||||
<Compile Include="Dithering\ColorError.cs" />
|
||||
<Compile Include="Dithering\FloydSteinbergDitherer.cs" />
|
||||
<Compile Include="ImageWriter.cs" />
|
||||
<Compile Include="ResourcesExtractor.cs" />
|
||||
<Compile Include="ResourcesHeader.cs" />
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<packages>
|
||||
<package id="NLog" version="4.4.12" targetFramework="net40-client" />
|
||||
</packages>
|
|
@ -10,7 +10,7 @@ namespace WatchFace.Parser.Elements.StatusElements
|
|||
|
||||
[ParameterId(2)]
|
||||
[ParameterImageIndex]
|
||||
public long ImageIndexOn { get; set; }
|
||||
public long? ImageIndexOn { get; set; }
|
||||
|
||||
[ParameterId(3)]
|
||||
[ParameterImageIndex]
|
||||
|
|
|
@ -18,7 +18,7 @@ namespace WatchFace.Parser.JsonConverters
|
|||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue,
|
||||
JsonSerializer serializer)
|
||||
{
|
||||
var str = (string)reader.Value;
|
||||
var str = (string) reader.Value;
|
||||
if (str == null || !str.StartsWith("0x"))
|
||||
throw new JsonSerializationException();
|
||||
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
|
||||
// by using the '*' as shown below:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.0.0.1")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.1")]
|
||||
[assembly: AssemblyVersion("1.0.0.2")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.2")]
|
|
@ -2,8 +2,6 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using NLog;
|
||||
using WatchFace.Parser.Attributes;
|
||||
|
||||
|
@ -42,8 +40,10 @@ namespace WatchFace.Parser.Utils
|
|||
var propertyType = propertyInfo.PropertyType;
|
||||
dynamic propertyValue = propertyInfo.GetValue(serializable, null);
|
||||
|
||||
var imageIndexAttribute =ElementsHelper.GetCustomAttributeFor<ParameterImageIndexAttribute>(propertyInfo);
|
||||
var imagesCountAttribute =ElementsHelper.GetCustomAttributeFor<ParameterImagesCountAttribute>(propertyInfo);
|
||||
var imageIndexAttribute =
|
||||
ElementsHelper.GetCustomAttributeFor<ParameterImageIndexAttribute>(propertyInfo);
|
||||
var imagesCountAttribute =
|
||||
ElementsHelper.GetCustomAttributeFor<ParameterImagesCountAttribute>(propertyInfo);
|
||||
|
||||
if (imagesCountAttribute != null && imageIndexAttribute != null)
|
||||
throw new ArgumentException(
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using NLog;
|
||||
using WatchFace.Parser.Attributes;
|
||||
using WatchFace.Parser.Models;
|
||||
|
||||
namespace WatchFace.Parser.Utils
|
||||
|
|
|
@ -90,7 +90,7 @@ namespace WatchFace.Parser
|
|||
|
||||
var encodedImage = new MemoryStream();
|
||||
Logger.Debug("Writing image {0}...", i);
|
||||
new ImageWriter(encodedImage, _images[i]).Write();
|
||||
new ImageWriter(encodedImage).Write(_images[i]);
|
||||
offset += (uint) encodedImage.Length;
|
||||
encodedImages[i] = encodedImage;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<packages>
|
||||
<package id="Newtonsoft.Json" version="10.0.3" 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>
|
||||
<startup>
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0,Profile=Client"/>
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0,Profile=Client" />
|
||||
</startup>
|
||||
</configuration>
|
||||
</configuration>
|
|
@ -21,7 +21,8 @@ namespace WatchFace
|
|||
{
|
||||
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("Usage examples:");
|
||||
Console.WriteLine(" {0}.exe watchface.bin - unpacks watchface images and config", AppName);
|
||||
|
@ -64,7 +65,7 @@ namespace WatchFace
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Fatal(e.Message);
|
||||
Logger.Fatal(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -80,7 +81,15 @@ namespace WatchFace
|
|||
if (watchFace == null) return;
|
||||
|
||||
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)
|
||||
|
|
|
@ -31,5 +31,5 @@ using System.Runtime.InteropServices;
|
|||
// You can specify all the values or you can default the Build and Revision Numbers
|
||||
// by using the '*' as shown below:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||
[assembly: AssemblyVersion("1.0.0.2")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.2")]
|
|
@ -1,4 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<packages>
|
||||
<package id="Newtonsoft.Json" version="10.0.3" targetFramework="net40-client" />
|
||||
<package id="NLog" version="4.4.12" targetFramework="net40-client" />
|
||||
|
|
Loading…
Reference in New Issue