From a6f36424e8988bdc63b00c044b36f077cc945a2d Mon Sep 17 00:00:00 2001 From: Valeriy Mironov Date: Tue, 28 Nov 2017 14:05:25 +0200 Subject: [PATCH] Added dithering for images on packing, added deleting incomplete bin if error happened on packing, closes #2 --- Resources/Dithering/ColorError.cs | 45 +++++++++++++++ Resources/Dithering/FloydSteinbergDitherer.cs | 57 +++++++++++++++++++ Resources/ImageReader.cs | 3 +- Resources/ImageWriter.cs | 57 ++++++++++++------- Resources/Properties/AssemblyInfo.cs | 4 +- Resources/Resources.csproj | 2 + Resources/packages.config | 1 + .../Elements/StatusElements/Switch.cs | 2 +- .../JsonConverters/HexStringJsonConverter.cs | 2 +- WatchFace.Parser/Properties/AssemblyInfo.cs | 4 +- WatchFace.Parser/Utils/ImagesLoader.cs | 8 +-- WatchFace.Parser/Utils/ParametersConverter.cs | 2 - WatchFace.Parser/Writer.cs | 2 +- WatchFace.Parser/packages.config | 1 + WatchFace/App.config | 7 ++- WatchFace/Program.cs | 15 ++++- WatchFace/Properties/AssemblyInfo.cs | 4 +- WatchFace/packages.config | 1 + 18 files changed, 176 insertions(+), 41 deletions(-) create mode 100644 Resources/Dithering/ColorError.cs create mode 100644 Resources/Dithering/FloydSteinbergDitherer.cs diff --git a/Resources/Dithering/ColorError.cs b/Resources/Dithering/ColorError.cs new file mode 100644 index 0000000..a9a4e0d --- /dev/null +++ b/Resources/Dithering/ColorError.cs @@ -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)); + } + } +} \ No newline at end of file diff --git a/Resources/Dithering/FloydSteinbergDitherer.cs b/Resources/Dithering/FloydSteinbergDitherer.cs new file mode 100644 index 0000000..1ed6531 --- /dev/null +++ b/Resources/Dithering/FloydSteinbergDitherer.cs @@ -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)); + } + } + } + } + } +} \ No newline at end of file diff --git a/Resources/ImageReader.cs b/Resources/ImageReader.cs index ee765ad..8d80cdf 100644 --- a/Resources/ImageReader.cs +++ b/Resources/ImageReader.cs @@ -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); } } diff --git a/Resources/ImageWriter.cs b/Resources/ImageWriter.cs index 66f351c..783457d 100644 --- a/Resources/ImageWriter.cs +++ b/Resources/ImageWriter.cs @@ -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 _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(); } - 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); diff --git a/Resources/Properties/AssemblyInfo.cs b/Resources/Properties/AssemblyInfo.cs index 0c03a78..7b707fc 100644 --- a/Resources/Properties/AssemblyInfo.cs +++ b/Resources/Properties/AssemblyInfo.cs @@ -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")] \ No newline at end of file +[assembly: AssemblyVersion("1.0.0.2")] +[assembly: AssemblyFileVersion("1.0.0.2")] \ No newline at end of file diff --git a/Resources/Resources.csproj b/Resources/Resources.csproj index 78dc200..068ac22 100644 --- a/Resources/Resources.csproj +++ b/Resources/Resources.csproj @@ -46,6 +46,8 @@ + + diff --git a/Resources/packages.config b/Resources/packages.config index 84cdf27..3773367 100644 --- a/Resources/packages.config +++ b/Resources/packages.config @@ -1,4 +1,5 @@  + \ No newline at end of file diff --git a/WatchFace.Parser/Elements/StatusElements/Switch.cs b/WatchFace.Parser/Elements/StatusElements/Switch.cs index 7614489..5218349 100644 --- a/WatchFace.Parser/Elements/StatusElements/Switch.cs +++ b/WatchFace.Parser/Elements/StatusElements/Switch.cs @@ -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] diff --git a/WatchFace.Parser/JsonConverters/HexStringJsonConverter.cs b/WatchFace.Parser/JsonConverters/HexStringJsonConverter.cs index c7d2ac0..f8ea72a 100644 --- a/WatchFace.Parser/JsonConverters/HexStringJsonConverter.cs +++ b/WatchFace.Parser/JsonConverters/HexStringJsonConverter.cs @@ -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); diff --git a/WatchFace.Parser/Properties/AssemblyInfo.cs b/WatchFace.Parser/Properties/AssemblyInfo.cs index bbbb4d8..136bc27 100644 --- a/WatchFace.Parser/Properties/AssemblyInfo.cs +++ b/WatchFace.Parser/Properties/AssemblyInfo.cs @@ -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")] \ No newline at end of file +[assembly: AssemblyVersion("1.0.0.2")] +[assembly: AssemblyFileVersion("1.0.0.2")] \ No newline at end of file diff --git a/WatchFace.Parser/Utils/ImagesLoader.cs b/WatchFace.Parser/Utils/ImagesLoader.cs index b3b1efc..896eec7 100644 --- a/WatchFace.Parser/Utils/ImagesLoader.cs +++ b/WatchFace.Parser/Utils/ImagesLoader.cs @@ -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(propertyInfo); - var imagesCountAttribute =ElementsHelper.GetCustomAttributeFor(propertyInfo); + var imageIndexAttribute = + ElementsHelper.GetCustomAttributeFor(propertyInfo); + var imagesCountAttribute = + ElementsHelper.GetCustomAttributeFor(propertyInfo); if (imagesCountAttribute != null && imageIndexAttribute != null) throw new ArgumentException( diff --git a/WatchFace.Parser/Utils/ParametersConverter.cs b/WatchFace.Parser/Utils/ParametersConverter.cs index dee8255..cb807b1 100644 --- a/WatchFace.Parser/Utils/ParametersConverter.cs +++ b/WatchFace.Parser/Utils/ParametersConverter.cs @@ -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 diff --git a/WatchFace.Parser/Writer.cs b/WatchFace.Parser/Writer.cs index bf4df6b..7ebef7b 100644 --- a/WatchFace.Parser/Writer.cs +++ b/WatchFace.Parser/Writer.cs @@ -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; } diff --git a/WatchFace.Parser/packages.config b/WatchFace.Parser/packages.config index 0cd68dd..ec8ab8b 100644 --- a/WatchFace.Parser/packages.config +++ b/WatchFace.Parser/packages.config @@ -1,4 +1,5 @@  + diff --git a/WatchFace/App.config b/WatchFace/App.config index 088ab21..ce3bd0d 100644 --- a/WatchFace/App.config +++ b/WatchFace/App.config @@ -1,6 +1,7 @@ - + + - + - + \ No newline at end of file diff --git a/WatchFace/Program.cs b/WatchFace/Program.cs index 882c9b3..88f725b 100644 --- a/WatchFace/Program.cs +++ b/WatchFace/Program.cs @@ -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) diff --git a/WatchFace/Properties/AssemblyInfo.cs b/WatchFace/Properties/AssemblyInfo.cs index 8dcd694..aa0302a 100644 --- a/WatchFace/Properties/AssemblyInfo.cs +++ b/WatchFace/Properties/AssemblyInfo.cs @@ -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")] \ No newline at end of file +[assembly: AssemblyVersion("1.0.0.2")] +[assembly: AssemblyFileVersion("1.0.0.2")] \ No newline at end of file diff --git a/WatchFace/packages.config b/WatchFace/packages.config index 0cd68dd..ec8ab8b 100644 --- a/WatchFace/packages.config +++ b/WatchFace/packages.config @@ -1,4 +1,5 @@  +