Added dithering for images on packing, added deleting incomplete bin if error happened on packing, closes #2

fonts_experiment 1.0.0.2
Valeriy Mironov 2017-11-28 14:05:25 +02:00
parent c5a1ed3fef
commit a6f36424e8
18 changed files with 176 additions and 41 deletions

View File

@ -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));
}
}
}

View File

@ -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));
}
}
}
}
}
}

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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")]

View File

@ -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" />

View File

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="NLog" version="4.4.12" targetFramework="net40-client" />
</packages>

View File

@ -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]

View File

@ -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);

View File

@ -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")]

View File

@ -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(

View File

@ -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

View File

@ -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;
}

View File

@ -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" />

View File

@ -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>

View File

@ -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)

View File

@ -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")]

View File

@ -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" />