From 3d0b01fe0d90756758d45961e475d884a7e45ebe Mon Sep 17 00:00:00 2001 From: Valeriy Mironov Date: Tue, 5 Dec 2017 03:29:46 +0200 Subject: [PATCH] Improved speed of decoding/encoding images, added generating animated previews, fixed drawing clock hands --- Amazfit.sln | 2 +- Resources/Image/FloydSteinbergDitherer.cs | 64 ++++++++-------- Resources/Image/Reader.cs | 18 +++-- Resources/Image/Writer.cs | 74 ++++++++++--------- Resources/Properties/AssemblyInfo.cs | 3 +- Resources/Resources.csproj | 3 + Resources/packages.config | 2 +- .../Elements/Common/ClockHandElement.cs | 7 +- .../Elements/Weather/WeatherIconsElement.cs | 11 +-- WatchFace.Parser/PreviewGenerator.cs | 63 ++++++++++++++++ WatchFace.Parser/Properties/AssemblyInfo.cs | 3 +- WatchFace.Parser/WatchFace.Parser.csproj | 12 ++- WatchFace.Parser/packages.config | 2 +- WatchFace/Program.cs | 27 +++---- WatchFace/Properties/AssemblyInfo.cs | 3 +- 15 files changed, 190 insertions(+), 104 deletions(-) create mode 100644 WatchFace.Parser/PreviewGenerator.cs diff --git a/Amazfit.sln b/Amazfit.sln index 8661403..7752d2b 100644 --- a/Amazfit.sln +++ b/Amazfit.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.27004.2009 +VisualStudioVersion = 15.0.27130.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WatchFace.Parser", "WatchFace.Parser\WatchFace.Parser.csproj", "{8D2E0224-DE7F-4EC9-AD23-9904B9D7B409}" EndProject diff --git a/Resources/Image/FloydSteinbergDitherer.cs b/Resources/Image/FloydSteinbergDitherer.cs index f52023a..be7ae04 100644 --- a/Resources/Image/FloydSteinbergDitherer.cs +++ b/Resources/Image/FloydSteinbergDitherer.cs @@ -1,4 +1,5 @@ using System.Drawing; +using BumpKit; using NLog; namespace Resources.Image @@ -10,45 +11,48 @@ namespace Resources.Image 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++) + using (var context = image.CreateUnsafeContext()) { - var color = image.GetPixel(x, y); - var colorError = new ColorError(color); - - if (colorError.IsZero) continue; - - if (!isImageAltered) + for (var y = 0; y < context.Height; y++) + for (var x = 0; x < context.Width; x++) { - 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; - } + var color = context.GetPixel(x, y); + var colorError = new ColorError(color); - image.SetPixel(x, y, colorError.NewColor); + if (colorError.IsZero) continue; - 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) + if (!isImageAltered) { - color = image.GetPixel(x - 1, y + 1); - image.SetPixel(x - 1, y + 1, colorError.ApplyError(color, 3, 16)); + 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; } - color = image.GetPixel(x, y + 1); - image.SetPixel(x, y + 1, colorError.ApplyError(color, 5, 16)); + context.SetPixel(x, y, colorError.NewColor); - if (x < image.Width - 1) + if (x + 1 < context.Width) { - color = image.GetPixel(x + 1, y + 1); - image.SetPixel(x + 1, y + 1, colorError.ApplyError(color, 1, 16)); + color = context.GetPixel(x + 1, y); + context.SetPixel(x + 1, y, colorError.ApplyError(color, 7, 16)); + } + + if (y + 1 < context.Height) + { + if (x > 1) + { + color = context.GetPixel(x - 1, y + 1); + context.SetPixel(x - 1, y + 1, colorError.ApplyError(color, 3, 16)); + } + + color = context.GetPixel(x, y + 1); + context.SetPixel(x, y + 1, colorError.ApplyError(color, 5, 16)); + + if (x < context.Width - 1) + { + color = context.GetPixel(x + 1, y + 1); + context.SetPixel(x + 1, y + 1, colorError.ApplyError(color, 1, 16)); + } } } } diff --git a/Resources/Image/Reader.cs b/Resources/Image/Reader.cs index 517fc15..8e1e4fb 100644 --- a/Resources/Image/Reader.cs +++ b/Resources/Image/Reader.cs @@ -1,6 +1,7 @@ using System; using System.Drawing; using System.IO; +using BumpKit; using NLog; using Resources.Utils; @@ -79,15 +80,18 @@ namespace Resources.Image private Bitmap ReadImage() { var image = new Bitmap(_width, _height); - for (var y = 0; y < _height; y++) + using (var context = image.CreateUnsafeContext()) { - var rowBytes = _reader.ReadBytes(_rowLengthInBytes); - var bitReader = new BitReader(rowBytes); - for (var x = 0; x < _width; x++) + for (var y = 0; y < _height; y++) { - var pixelColorIndex = bitReader.ReadBits(_bitsPerPixel); - var color = _palette[(int) pixelColorIndex]; - image.SetPixel(x, y, color); + var rowBytes = _reader.ReadBytes(_rowLengthInBytes); + var bitReader = new BitReader(rowBytes); + for (var x = 0; x < _width; x++) + { + var pixelColorIndex = bitReader.ReadBits(_bitsPerPixel); + var color = _palette[(int) pixelColorIndex]; + context.SetPixel(x, y, color); + } } } return image; diff --git a/Resources/Image/Writer.cs b/Resources/Image/Writer.cs index 2fb0af8..cb6c131 100644 --- a/Resources/Image/Writer.cs +++ b/Resources/Image/Writer.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Drawing; using System.Drawing.Imaging; using System.IO; +using BumpKit; using NLog; using Resources.Utils; @@ -70,26 +71,30 @@ namespace Resources.Image private void ExtractPalette() { Logger.Trace("Extracting palette..."); - for (var y = 0; y < _height; y++) - for (var x = 0; x < _width; x++) - { - var color = _image.GetPixel(x, y); - if (_palette.Contains(color)) continue; - if (color.A < 0x80 && _transparency == 0) + using (var context = _image.CreateUnsafeContext()) + { + for (var y = 0; y < _height; y++) + for (var x = 0; x < _width; x++) { - 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.Insert(0, color); - _transparency = 1; - } - else - { - Logger.Trace("Palette item {0}: R {1:X2}, G {2:X2}, B {3:X2}", - _palette.Count, color.R, color.G, color.B - ); - _palette.Add(color); + var color = context.GetPixel(x, y); + if (_palette.Contains(color)) continue; + + 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 + ); + _palette.Insert(0, color); + _transparency = 1; + } + else + { + Logger.Trace("Palette item {0}: R {1:X2}, G {2:X2}, B {3:X2}", + _palette.Count, color.R, color.G, color.B + ); + _palette.Add(color); + } } } _paletteColors = (ushort) _palette.Count; @@ -136,26 +141,29 @@ namespace Resources.Image i++; } - for (var y = 0; y < _height; y++) + using (var context = _image.CreateUnsafeContext()) { - var rowData = new byte[_rowLengthInBytes]; - var memoryStream = new MemoryStream(rowData); - var bitWriter = new BitWriter(memoryStream); - for (var x = 0; x < _width; x++) + for (var y = 0; y < _height; y++) { - var color = _image.GetPixel(x, y); - if (color.A < 0x80 && _transparency == 1) + var rowData = new byte[_rowLengthInBytes]; + var memoryStream = new MemoryStream(rowData); + var bitWriter = new BitWriter(memoryStream); + for (var x = 0; x < _width; x++) { - bitWriter.WriteBits(0, _bitsPerPixel); - } - else - { - var paletteIndex = paletteHash[color]; - bitWriter.WriteBits(paletteIndex, _bitsPerPixel); + var color = context.GetPixel(x, y); + if (color.A < 0x80 && _transparency == 1) + { + bitWriter.WriteBits(0, _bitsPerPixel); + } + else + { + var paletteIndex = paletteHash[color]; + bitWriter.WriteBits(paletteIndex, _bitsPerPixel); + } } + bitWriter.Flush(); + _writer.Write(rowData); } - bitWriter.Flush(); - _writer.Write(rowData); } } } diff --git a/Resources/Properties/AssemblyInfo.cs b/Resources/Properties/AssemblyInfo.cs index f072c49..b79d147 100644 --- a/Resources/Properties/AssemblyInfo.cs +++ b/Resources/Properties/AssemblyInfo.cs @@ -31,5 +31,4 @@ 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.1.0")] -[assembly: AssemblyFileVersion("1.0.1.0")] \ No newline at end of file +[assembly: AssemblyVersion("1.0.*")] \ No newline at end of file diff --git a/Resources/Resources.csproj b/Resources/Resources.csproj index b3f2504..cd3a60f 100644 --- a/Resources/Resources.csproj +++ b/Resources/Resources.csproj @@ -31,6 +31,9 @@ 4 + + ..\packages\Bumpkit.1.0.2\lib\BumpKit.dll + ..\packages\NLog.4.4.12\lib\net40\NLog.dll diff --git a/Resources/packages.config b/Resources/packages.config index 3773367..e7a2a3e 100644 --- a/Resources/packages.config +++ b/Resources/packages.config @@ -1,5 +1,5 @@  - + \ No newline at end of file diff --git a/WatchFace.Parser/Models/Elements/Common/ClockHandElement.cs b/WatchFace.Parser/Models/Elements/Common/ClockHandElement.cs index c634651..df71d10 100644 --- a/WatchFace.Parser/Models/Elements/Common/ClockHandElement.cs +++ b/WatchFace.Parser/Models/Elements/Common/ClockHandElement.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Drawing; +using System.Drawing.Drawing2D; using System.Linq; using WatchFace.Parser.Interfaces; @@ -29,7 +30,7 @@ namespace WatchFace.Parser.Models.Elements.Common } else { - drawer.FillPolygon(new SolidBrush(Color), points); + drawer.FillPolygon(new SolidBrush(Color), points, FillMode.Alternate); drawer.DrawPolygon(new Pen(Color, 1), points); } @@ -40,8 +41,8 @@ namespace WatchFace.Parser.Models.Elements.Common { var radians = degrees / 180 * Math.PI; var x = element.X * Math.Cos(radians) + element.Y * Math.Sin(radians); - var y = element.X * Math.Sin(radians) + element.Y * Math.Cos(radians); - return new Point((int) Math.Round(x + Center.X), (int) Math.Round(y + Center.Y)); + var y = element.X * Math.Sin(radians) - element.Y * Math.Cos(radians); + return new Point((int) Math.Floor(x + Center.X), (int) Math.Floor(y + Center.Y)); } protected override Element CreateChildForParameter(Parameter parameter) diff --git a/WatchFace.Parser/Models/Elements/Weather/WeatherIconsElement.cs b/WatchFace.Parser/Models/Elements/Weather/WeatherIconsElement.cs index f0fc722..1fdf57a 100644 --- a/WatchFace.Parser/Models/Elements/Weather/WeatherIconsElement.cs +++ b/WatchFace.Parser/Models/Elements/Weather/WeatherIconsElement.cs @@ -15,18 +15,19 @@ namespace WatchFace.Parser.Models.Elements public void Draw(Graphics drawer, Bitmap[] resources, WatchState state) { - if (state.CurrentWeather != WeatherCondition.Unknown) + if (state.CurrentWeather != WeatherCondition.Unknown && Current != null) drawer.DrawImage(LoadWeatherImage(state.CurrentWeather), Current.X, Current.Y); - else if (state.TodayWeather != WeatherCondition.Unknown) + else if (state.TodayWeather != WeatherCondition.Unknown && Today != null) drawer.DrawImage(LoadWeatherImage(state.TodayWeather), Today.X, Today.Y); - else if (state.TomorrowWeather != WeatherCondition.Unknown) + else if (state.TomorrowWeather != WeatherCondition.Unknown && Tomorrow != null) drawer.DrawImage(LoadWeatherImage(state.TomorrowWeather), Tomorrow.X, Tomorrow.Y); } private static Bitmap LoadWeatherImage(WeatherCondition weather) { - var appLocation = System.IO.Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); - return (Bitmap) Image.FromFile(System.IO.Path.Combine(appLocation, "WeatherIcons", $"{(int) weather}.png")); + var assembly = Assembly.GetExecutingAssembly(); + var imageStream = assembly.GetManifestResourceStream($"WatchFace.Parser.WeatherIcons.{(int) weather}.png"); + return (Bitmap) Image.FromStream(imageStream); } protected override Element CreateChildForParameter(Parameter parameter) diff --git a/WatchFace.Parser/PreviewGenerator.cs b/WatchFace.Parser/PreviewGenerator.cs new file mode 100644 index 0000000..2c0e588 --- /dev/null +++ b/WatchFace.Parser/PreviewGenerator.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using BumpKit; +using WatchFace.Parser.Interfaces; +using WatchFace.Parser.Models; + +namespace WatchFace.Parser +{ + public class PreviewGenerator + { + public static void CreateGif(List descriptor, Bitmap[] images, Stream outputStream) + { + var previewWatchFace = new Models.Elements.WatchFace(descriptor); + var watchState = new WatchState(); + var time = watchState.Time; + + using (var encoder = new GifEncoder(outputStream)) + { + for (var i = 0; i < 10; i++) + { + var num = i + 1; + watchState.BatteryLevel = num * 10; + + watchState.Pulse = 60 + num * 2; + watchState.Steps = num * 1000; + watchState.Calories = num * 75; + watchState.Distance = num * 700; + + watchState.Bluetooth = num > 1 && num < 6; + watchState.Unlocked = num > 2 && num < 7; + watchState.Alarm = num > 3 && num < 8; + watchState.DoNotDisturb = num > 4 && num < 9; + + watchState.DayTemperature += 2; + watchState.NightTemperature += 4; + watchState.CurrentTemperature += 3; + + watchState.Time = new DateTime(time.Year, num, num * 2 + 5, i * 2, i * 6, i); + using (var image = CreateFrame(previewWatchFace, images, watchState)) + { + encoder.AddFrame(image, frameDelay: TimeSpan.FromSeconds(1)); + } + } + } + } + + public static Image CreateImage(IEnumerable descriptor, Bitmap[] resources) + { + var previewWatchFace = new Models.Elements.WatchFace(descriptor); + return CreateFrame(previewWatchFace, resources, new WatchState()); + } + + private static Image CreateFrame(IDrawable watchFace, Bitmap[] resources, WatchState state) + { + var preview = new Bitmap(176, 176); + var graphics = Graphics.FromImage(preview); + watchFace.Draw(graphics, resources, state); + return preview; + } + } +} \ No newline at end of file diff --git a/WatchFace.Parser/Properties/AssemblyInfo.cs b/WatchFace.Parser/Properties/AssemblyInfo.cs index 01644bb..a82da8f 100644 --- a/WatchFace.Parser/Properties/AssemblyInfo.cs +++ b/WatchFace.Parser/Properties/AssemblyInfo.cs @@ -31,5 +31,4 @@ 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.1.0")] -[assembly: AssemblyFileVersion("1.0.1.0")] \ No newline at end of file +[assembly: AssemblyVersion("1.0.*")] \ No newline at end of file diff --git a/WatchFace.Parser/WatchFace.Parser.csproj b/WatchFace.Parser/WatchFace.Parser.csproj index e971636..54e7c46 100644 --- a/WatchFace.Parser/WatchFace.Parser.csproj +++ b/WatchFace.Parser/WatchFace.Parser.csproj @@ -31,6 +31,9 @@ 4 + + ..\packages\Bumpkit.1.0.2\lib\BumpKit.dll + ..\packages\Newtonsoft.Json.10.0.3\lib\net40\Newtonsoft.Json.dll @@ -148,6 +151,7 @@ + @@ -169,12 +173,12 @@ - + Always - - + + Always - + diff --git a/WatchFace.Parser/packages.config b/WatchFace.Parser/packages.config index ec8ab8b..8df43ff 100644 --- a/WatchFace.Parser/packages.config +++ b/WatchFace.Parser/packages.config @@ -1,6 +1,6 @@  - + \ No newline at end of file diff --git a/WatchFace/Program.cs b/WatchFace/Program.cs index 643c84d..6ae37d0 100644 --- a/WatchFace/Program.cs +++ b/WatchFace/Program.cs @@ -9,7 +9,7 @@ using NLog.Config; using NLog.Targets; using Resources; using Resources.Models; -using WatchFace.Parser.Models; +using WatchFace.Parser; using WatchFace.Parser.Utils; using Reader = WatchFace.Parser.Reader; using Writer = WatchFace.Parser.Writer; @@ -126,13 +126,13 @@ namespace WatchFace var watchFace = ParseResources(reader); if (watchFace == null) return; - Logger.Debug("Generating preview..."); - var previewWatchFace = new Parser.Models.Elements.WatchFace(reader.Parameters); - - var preview = new Bitmap(176, 176); - var graphics = Graphics.FromImage(preview); - previewWatchFace.Draw(graphics, reader.Images.ToArray(), new WatchState()); - preview.Save(Path.Combine(outputDirectory, $"{baseName}.png"), ImageFormat.Png); + Logger.Debug("Generating previews..."); + var preview = PreviewGenerator.CreateImage(reader.Parameters, reader.Images.ToArray()); + preview.Save(Path.Combine(outputDirectory, $"{baseName}_preview.png"), ImageFormat.Png); + using (var gifOutput = File.OpenWrite(Path.Combine(outputDirectory, $"{baseName}_preview.gif"))) + { + PreviewGenerator.CreateGif(reader.Parameters, reader.Images.ToArray(), gifOutput); + } Logger.Debug("Exporting resources to '{0}'", outputDirectory); var reDescriptor = new FileDescriptor {Images = reader.Images}; @@ -200,12 +200,12 @@ namespace WatchFace var descriptor = ParametersConverter.Build(watchFace); Logger.Debug("Generating preview..."); - var previewWatchFace = new Parser.Models.Elements.WatchFace(descriptor); - - var preview = new Bitmap(176, 176); - var graphics = Graphics.FromImage(preview); - previewWatchFace.Draw(graphics, imagesReader.Images.ToArray(), new WatchState()); + var preview = PreviewGenerator.CreateImage(descriptor, imagesReader.Images.ToArray()); preview.Save(Path.ChangeExtension(outputFileName, ".png")); + using (var gifOutput = File.OpenWrite(Path.ChangeExtension(outputFileName, ".gif"))) + { + PreviewGenerator.CreateGif(descriptor, imagesReader.Images.ToArray(), gifOutput); + } Logger.Debug("Writing watch face to '{0}'", outputFileName); using (var fileStream = File.OpenWrite(outputFileName)) @@ -218,6 +218,7 @@ namespace WatchFace catch (Exception e) { Logger.Fatal(e.Message); + File.Delete(outputFileName); } } diff --git a/WatchFace/Properties/AssemblyInfo.cs b/WatchFace/Properties/AssemblyInfo.cs index 145e215..9cfff0d 100644 --- a/WatchFace/Properties/AssemblyInfo.cs +++ b/WatchFace/Properties/AssemblyInfo.cs @@ -31,5 +31,4 @@ 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.1.0")] -[assembly: AssemblyFileVersion("1.0.1.0")] \ No newline at end of file +[assembly: AssemblyVersion("1.0.*")] \ No newline at end of file