#637 Conversion to use ImageSharp rather than System.Drawing.Common for cross platform compatibility.

Breaking Changes:

* `System.Drawing.Common` is dropped and `SixLabors.ImageSharp` is introduced.
* uses of `NativeImage.CreateFromBitmap(bitmap, options);` is no longer supported, will cause failed builds.

Unexpected Behaviors:

* uses ToDataUrl will always create png data urls, unexpected output may happen for those that manipulate this output expecting a different data url format.

Obsoletions:

* `CreateFromBitmapOptions` & `CreateFromBufferOptions` have been consolidated into `CreateOptions`. Implicit conversions added to ease transition.
* `ToBitmapOptions`, `ToDataUrlOptions`, `ToPngOptions` & `BitmapOptions` have been consolidated into `ImageOptions`. Implicit conversions added to ease transition.
This commit is contained in:
Daniel Gidman
2022-01-26 11:07:48 -06:00
parent c2a8c627b9
commit 86644e7366
13 changed files with 186 additions and 155 deletions

View File

@@ -40,8 +40,8 @@ This package contains the API to access the "native" electron API.</Description>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.4" />
<PackageReference Include="SocketIoClientDotNet" Version="1.0.5" /> <PackageReference Include="SocketIoClientDotNet" Version="1.0.5" />
<PackageReference Include="System.Drawing.Common" Version="5.0.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -18,7 +18,7 @@ namespace ElectronNET.API.Entities
/// <summary> /// <summary>
/// Gets or sets the scalefactor /// Gets or sets the scalefactor
/// </summary> /// </summary>
public float ScaleFactor { get; set; } = 1.0f; public float ScaleFactor { get; set; } = NativeImage.DefaultScaleFactor;
/// <summary> /// <summary>
/// Gets or sets the buffer /// Gets or sets the buffer

View File

@@ -1,13 +1,20 @@
namespace ElectronNET.API.Entities using System;
namespace ElectronNET.API.Entities
{ {
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
[Obsolete("Use ImageOptions instead.")]
public class BitmapOptions public class BitmapOptions
{ {
/// <summary> /// <summary>
/// Gets or sets the scale factor /// Gets or sets the scale factor
/// </summary> /// </summary>
public float ScaleFactor { get; set; } = 1.0f; public float ScaleFactor { get; set; } = NativeImage.DefaultScaleFactor;
/// <summary>
/// Utility conversion for obsolete class
/// </summary>
public static implicit operator ImageOptions(BitmapOptions o) => new() {ScaleFactor = o.ScaleFactor};
} }
} }

View File

@@ -1,8 +1,11 @@
namespace ElectronNET.API.Entities using System;
namespace ElectronNET.API.Entities
{ {
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
[Obsolete("Use CreateOptions instead")]
public class CreateFromBitmapOptions public class CreateFromBitmapOptions
{ {
/// <summary> /// <summary>
@@ -18,6 +21,12 @@
/// <summary> /// <summary>
/// Gets or sets the scalefactor /// Gets or sets the scalefactor
/// </summary> /// </summary>
public float ScaleFactor { get; set; } = 1.0f; public float ScaleFactor { get; set; } = NativeImage.DefaultScaleFactor;
/// <summary>
/// Utility conversion for obsolete class
/// </summary>
public static implicit operator CreateOptions(CreateFromBitmapOptions o) => new()
{Width = o.Width, Height = o.Height, ScaleFactor = o.ScaleFactor};
} }
} }

View File

@@ -1,8 +1,11 @@
using System;
namespace ElectronNET.API.Entities namespace ElectronNET.API.Entities
{ {
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
[Obsolete("Use CreateOptions instead")]
public class CreateFromBufferOptions public class CreateFromBufferOptions
{ {
/// <summary> /// <summary>
@@ -18,6 +21,11 @@ namespace ElectronNET.API.Entities
/// <summary> /// <summary>
/// Gets or sets the scalefactor /// Gets or sets the scalefactor
/// </summary> /// </summary>
public float ScaleFactor { get; set; } = 1.0f; public float ScaleFactor { get; set; } = NativeImage.DefaultScaleFactor;
/// <summary>
/// Utility conversion for obsolete class
/// </summary>
public static implicit operator CreateOptions(CreateFromBufferOptions o) => new()
{Width = o.Width, Height = o.Height, ScaleFactor = o.ScaleFactor};
} }
} }

View File

@@ -0,0 +1,24 @@
namespace ElectronNET.API.Entities
{
/// <summary>
/// Options for creating a new <see cref="NativeImage"/>
/// </summary>
public class CreateOptions
{
/// <summary>
/// Gets or sets the width
/// </summary>
public int? Width { get; set; }
/// <summary>
/// Gets or sets the height
/// </summary>
public int? Height { get; set; }
/// <summary>
/// Gets or sets the scalefactor
/// </summary>
public float ScaleFactor { get; set; } = NativeImage.DefaultScaleFactor;
}
}

View File

@@ -0,0 +1,13 @@
namespace ElectronNET.API.Entities
{
/// <summary>
///
/// </summary>
public class ImageOptions
{
/// <summary>
/// Gets or sets the scale factor
/// </summary>
public float ScaleFactor { get; set; } = NativeImage.DefaultScaleFactor;
}
}

View File

@@ -1,12 +1,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Newtonsoft.Json; using Newtonsoft.Json;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Processing;
namespace ElectronNET.API.Entities namespace ElectronNET.API.Entities
{ {
@@ -16,6 +16,11 @@ namespace ElectronNET.API.Entities
[JsonConverter(typeof(NativeImageJsonConverter))] [JsonConverter(typeof(NativeImageJsonConverter))]
public class NativeImage public class NativeImage
{ {
/// <summary>
///
/// </summary>
public const float DefaultScaleFactor = 1.0f;
private readonly Dictionary<float, Image> _images = new Dictionary<float, Image>(); private readonly Dictionary<float, Image> _images = new Dictionary<float, Image>();
private bool _isTemplateImage; private bool _isTemplateImage;
@@ -37,7 +42,7 @@ namespace ElectronNET.API.Entities
private static Image BytesToImage(byte[] bytes) private static Image BytesToImage(byte[] bytes)
{ {
var ms = new MemoryStream(bytes); var ms = new MemoryStream(bytes);
return Image.FromStream(ms); return Image.Load(ms);
} }
/// <summary> /// <summary>
@@ -51,30 +56,28 @@ namespace ElectronNET.API.Entities
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
public static NativeImage CreateFromBitmap(Bitmap bitmap, CreateFromBitmapOptions options = null) [Obsolete("System.Drawing.Common is no longer supported. Use NativeImage.CreateFromImage(image, options);", true)]
public static NativeImage CreateFromBitmap(object bitmap, CreateOptions options = null)
{ {
if (options is null) throw new NotImplementedException(
{ "System.Drawing.Common is no longer supported. Use NativeImage.CreateFromImage(image, options);");
options = new CreateFromBitmapOptions();
}
return new NativeImage(bitmap, options.ScaleFactor);
} }
/// <summary>
///
/// </summary>
public static NativeImage CreateFromImage(Image image, CreateOptions options = null)
=> new (image, options?.ScaleFactor ?? DefaultScaleFactor);
/// <summary> /// <summary>
/// Creates a NativeImage from a byte array. /// Creates a NativeImage from a byte array.
/// </summary> /// </summary>
public static NativeImage CreateFromBuffer(byte[] buffer, CreateFromBufferOptions options = null) public static NativeImage CreateFromBuffer(byte[] buffer, CreateOptions options = null)
{ {
if (options is null)
{
options = new CreateFromBufferOptions();
}
var ms = new MemoryStream(buffer); var ms = new MemoryStream(buffer);
var image = Image.FromStream(ms); var image = Image.Load(ms);
return new NativeImage(image, options.ScaleFactor); return new NativeImage(image, options?.ScaleFactor ?? DefaultScaleFactor);
} }
/// <summary> /// <summary>
@@ -110,14 +113,14 @@ namespace ElectronNET.API.Entities
throw new Exception($"Invalid scaling factor for '{path}'."); throw new Exception($"Invalid scaling factor for '{path}'.");
} }
images[dpi.Value] = Image.FromFile(path); images[dpi.Value] = Image.Load(path);
} }
else else
{ {
var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(path); var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(path);
var extension = Path.GetExtension(path); var extension = Path.GetExtension(path);
// Load as 1x dpi // Load as 1x dpi
images[1.0f] = Image.FromFile(path); images[1.0f] = Image.Load(path);
foreach (var scale in ScaleFactorPairs) foreach (var scale in ScaleFactorPairs)
{ {
@@ -127,7 +130,7 @@ namespace ElectronNET.API.Entities
var dpi = ExtractDpiFromFilePath(fileName); var dpi = ExtractDpiFromFilePath(fileName);
if (dpi != null) if (dpi != null)
{ {
images[dpi.Value] = Image.FromFile(fileName); images[dpi.Value] = Image.Load(fileName);
} }
} }
} }
@@ -146,9 +149,9 @@ namespace ElectronNET.API.Entities
/// <summary> /// <summary>
/// Creates a NativeImage from a bitmap and scale factor /// Creates a NativeImage from a bitmap and scale factor
/// </summary> /// </summary>
public NativeImage(Image bitmap, float scaleFactor = 1.0f) public NativeImage(Image image, float scaleFactor = DefaultScaleFactor)
{ {
_images.Add(scaleFactor, bitmap); _images.Add(scaleFactor, image);
} }
/// <summary> /// <summary>
@@ -196,7 +199,7 @@ namespace ElectronNET.API.Entities
if (options.Buffer.Length > 0) if (options.Buffer.Length > 0)
{ {
_images[options.ScaleFactor] = _images[options.ScaleFactor] =
CreateFromBuffer(options.Buffer, new CreateFromBufferOptions {ScaleFactor = options.ScaleFactor}) CreateFromBuffer(options.Buffer, new CreateOptions {ScaleFactor = options.ScaleFactor})
.GetScale(options.ScaleFactor); .GetScale(options.ScaleFactor);
} }
else if (!string.IsNullOrEmpty(options.DataUrl)) else if (!string.IsNullOrEmpty(options.DataUrl))
@@ -214,7 +217,7 @@ namespace ElectronNET.API.Entities
var image = GetScale(scaleFactor); var image = GetScale(scaleFactor);
if (image != null) if (image != null)
{ {
return image.Width / image.Height; return Convert.ToSingle(image.Width) / image.Height;
} }
return 0f; return 0f;
@@ -223,9 +226,9 @@ namespace ElectronNET.API.Entities
/// <summary> /// <summary>
/// Returns a byte array that contains the image's raw bitmap pixel data. /// Returns a byte array that contains the image's raw bitmap pixel data.
/// </summary> /// </summary>
public byte[] GetBitmap(BitmapOptions options) public byte[] GetBitmap(ImageOptions options)
{ {
return ToBitmap(new ToBitmapOptions{ ScaleFactor = options.ScaleFactor }); return ToBitmap(options);
} }
/// <summary> /// <summary>
@@ -233,26 +236,14 @@ namespace ElectronNET.API.Entities
/// </summary> /// </summary>
public byte[] GetNativeHandle() public byte[] GetNativeHandle()
{ {
return ToBitmap(new ToBitmapOptions()); return ToBitmap(new ImageOptions());
} }
/// <summary> /// <summary>
/// Gets the size of the specified image based on scale factor /// Gets the size of the specified image based on scale factor
/// </summary> /// </summary>
public Size GetSize(float scaleFactor = 1.0f) public Size GetSize(float scaleFactor = 1.0f)
{ => _images.TryGetValue(scaleFactor, out var image) ? image.Size() : null;
if (_images.ContainsKey(scaleFactor))
{
var image = _images[scaleFactor];
return new Size
{
Width = image.Width,
Height = image.Height
};
}
return null;
}
/// <summary> /// <summary>
/// Checks to see if the NativeImage instance is empty. /// Checks to see if the NativeImage instance is empty.
@@ -278,104 +269,64 @@ namespace ElectronNET.API.Entities
/// <summary> /// <summary>
/// Outputs a bitmap based on the scale factor /// Outputs a bitmap based on the scale factor
/// </summary> /// </summary>
public byte[] ToBitmap(ToBitmapOptions options) public byte[] ToBitmap(ImageOptions options)
{ {
return ImageToBytes(ImageFormat.Bmp, options.ScaleFactor); var ms = new MemoryStream();
_images[options.ScaleFactor].SaveAsBmp(ms);
return ms.ToArray();
} }
/// <summary> /// <summary>
/// Outputs a data URL based on the scale factor /// Outputs a data URL based on the scale factor
/// </summary> /// </summary>
public string ToDataURL(ToDataUrlOptions options) public string ToDataURL(ImageOptions options)
{ => _images.TryGetValue(options.ScaleFactor, out var image)
if (!_images.ContainsKey(options.ScaleFactor)) ? $"data:image/png;base64,{image.ToBase64String(PngFormat.Instance)}"
{ : null;
return null;
}
var image = _images[options.ScaleFactor];
var mimeType = ImageCodecInfo.GetImageEncoders().FirstOrDefault(x => x.FormatID == image.RawFormat.Guid)?.MimeType;
if (mimeType is null)
{
mimeType = "image/png";
}
var bytes = ImageToBytes(image.RawFormat, options.ScaleFactor);
var base64 = Convert.ToBase64String(bytes);
return $"data:{mimeType};base64,{base64}";
}
/// <summary> /// <summary>
/// Outputs a JPEG for the default scale factor /// Outputs a JPEG for the default scale factor
/// </summary> /// </summary>
public byte[] ToJPEG(int quality) public byte[] ToJPEG(int quality)
{ {
return ImageToBytes(ImageFormat.Jpeg, 1.0f, quality); var ms = new MemoryStream();
_images[1.0f].SaveAsJpeg(ms);
return ms.ToArray();
} }
/// <summary> /// <summary>
/// Outputs a PNG for the specified scale factor /// Outputs a PNG for the specified scale factor
/// </summary> /// </summary>
public byte[] ToPNG(ToPNGOptions options) public byte[] ToPNG(ImageOptions options)
{ {
return ImageToBytes(ImageFormat.Png, options.ScaleFactor); if (_images.TryGetValue(options.ScaleFactor, out var image))
}
private byte[] ImageToBytes(ImageFormat imageFormat = null, float scaleFactor = 1.0f, int quality = 100)
{
using var ms = new MemoryStream();
if (_images.ContainsKey(scaleFactor))
{ {
var image = _images[scaleFactor]; var ms = new MemoryStream();
var encoderCodecInfo = GetEncoder(imageFormat ?? image.RawFormat); image.SaveAsPng(ms);
var encoder = Encoder.Quality;
var encoderParameters = new EncoderParameters(1)
{
Param = new[]
{
new EncoderParameter(encoder, quality)
}
};
image.Save(ms, encoderCodecInfo, encoderParameters);
return ms.ToArray(); return ms.ToArray();
} }
return null; return null;
} }
private Image Resize(int? width, int? height, float scaleFactor = 1.0f) private Image Resize(int? width, int? height, float scaleFactor = 1.0f)
{ {
if (!_images.ContainsKey(scaleFactor) || (width is null && height is null)) if (!_images.TryGetValue(scaleFactor, out var image) || (width is null && height is null))
{ {
return null; return null;
} }
var aspect = GetAspectRatio(scaleFactor);
var image = _images[scaleFactor]; width ??= Convert.ToInt32(image.Width * aspect);
using (var g = Graphics.FromImage(image)) height ??= Convert.ToInt32(image.Height * aspect);
width = Convert.ToInt32(width * scaleFactor);
height = Convert.ToInt32(height * scaleFactor);
return image.Clone(c => c.Resize(new SixLabors.ImageSharp.Processing.ResizeOptions
{ {
g.CompositingQuality = CompositingQuality.HighQuality; Size = new (width.Value, height.Value),
Sampler = KnownResamplers.Triangle,
var aspect = GetAspectRatio(scaleFactor); }));
width ??= Convert.ToInt32(image.Width * aspect);
height ??= Convert.ToInt32(image.Height * aspect);
width = Convert.ToInt32(width * scaleFactor);
height = Convert.ToInt32(height * scaleFactor);
var bmp = new Bitmap(width.Value, height.Value);
g.DrawImage(bmp,
new System.Drawing.Rectangle(0, 0, image.Width, image.Height),
new System.Drawing.Rectangle(0, 0, bmp.Width, bmp.Height),
GraphicsUnit.Pixel);
return bmp;
}
} }
private Image Crop(int? x, int? y, int? width, int? height, float scaleFactor = 1.0f) private Image Crop(int? x, int? y, int? width, int? height, float scaleFactor = 1.0f)
@@ -386,40 +337,21 @@ namespace ElectronNET.API.Entities
} }
var image = _images[scaleFactor]; var image = _images[scaleFactor];
using (var g = Graphics.FromImage(image))
{
g.CompositingQuality = CompositingQuality.HighQuality;
x ??= 0; x ??= 0;
y ??= 0; y ??= 0;
x = Convert.ToInt32(x * scaleFactor); x = Convert.ToInt32(x * scaleFactor);
y = Convert.ToInt32(y * scaleFactor); y = Convert.ToInt32(y * scaleFactor);
width ??= image.Width; width ??= image.Width;
height ??= image.Height; height ??= image.Height;
width = Convert.ToInt32(width * scaleFactor); width = Convert.ToInt32(width * scaleFactor);
height = Convert.ToInt32(height * scaleFactor); height = Convert.ToInt32(height * scaleFactor);
var bmp = new Bitmap(width.Value, height.Value); return image.Clone(c =>
g.DrawImage(bmp, new System.Drawing.Rectangle(0, 0, image.Width, image.Height), new System.Drawing.Rectangle(x.Value, y.Value, width.Value, height.Value), GraphicsUnit.Pixel); c.Crop(new SixLabors.ImageSharp.Rectangle(x.Value, y.Value, width.Value, height.Value)));
return bmp;
}
}
private ImageCodecInfo GetEncoder(ImageFormat format)
{
var codecs = ImageCodecInfo.GetImageDecoders();
foreach (ImageCodecInfo codec in codecs)
{
if (codec.FormatID == format.Guid)
{
return codec;
}
}
return null;
} }
internal Dictionary<float,string> GetAllScaledImages() internal Dictionary<float,string> GetAllScaledImages()
@@ -429,7 +361,7 @@ namespace ElectronNET.API.Entities
{ {
foreach (var (scale, image) in _images) foreach (var (scale, image) in _images)
{ {
dict.Add(scale, Convert.ToBase64String(ImageToBytes(null, scale))); dict.Add(scale, image.ToBase64String(PngFormat.Instance));
} }
} }
catch (Exception ex) catch (Exception ex)
@@ -449,5 +381,10 @@ namespace ElectronNET.API.Entities
return null; return null;
} }
/// <summary>
/// Utility conversion operator
/// </summary>
public static implicit operator NativeImage(Image src) => CreateFromImage(src);
} }
} }

View File

@@ -1,8 +1,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Drawing;
using System.IO; using System.IO;
using Newtonsoft.Json; using Newtonsoft.Json;
using SixLabors.ImageSharp;
namespace ElectronNET.API.Entities namespace ElectronNET.API.Entities
{ {
@@ -24,7 +24,7 @@ namespace ElectronNET.API.Entities
foreach (var item in dict) foreach (var item in dict)
{ {
var bytes = Convert.FromBase64String(item.Value); var bytes = Convert.FromBase64String(item.Value);
newDictionary.Add(item.Key, Image.FromStream(new MemoryStream(bytes))); newDictionary.Add(item.Key, Image.Load(new MemoryStream(bytes)));
} }
return new NativeImage(newDictionary); return new NativeImage(newDictionary);
} }

View File

@@ -20,5 +20,17 @@
/// The height. /// The height.
/// </value> /// </value>
public int Height { get; set; } public int Height { get; set; }
/// <summary>
/// Utility implicit conversion
/// </summary>
public static implicit operator SixLabors.ImageSharp.Size(Size s) =>
new (s.Width, s.Height);
/// <summary>
/// Utility implicit conversion
/// </summary>
public static implicit operator Size(SixLabors.ImageSharp.Size s) =>
new (){Height = s.Height, Width = s.Width};
} }
} }

View File

@@ -1,13 +1,20 @@
namespace ElectronNET.API.Entities using System;
namespace ElectronNET.API.Entities
{ {
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
[Obsolete("Use ImageOptions instead.")]
public class ToBitmapOptions public class ToBitmapOptions
{ {
/// <summary> /// <summary>
/// Gets or sets the scalefactor /// Gets or sets the scalefactor
/// </summary> /// </summary>
public float ScaleFactor { get; set; } = 1.0f; public float ScaleFactor { get; set; } = NativeImage.DefaultScaleFactor;
/// <summary>
/// Utility conversion for obsolete class
/// </summary>
public static implicit operator ImageOptions(ToBitmapOptions o) => new () {ScaleFactor = o.ScaleFactor};
} }
} }

View File

@@ -1,13 +1,20 @@
namespace ElectronNET.API.Entities using System;
namespace ElectronNET.API.Entities
{ {
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
[Obsolete("Use ImageOptions instead.")]
public class ToDataUrlOptions public class ToDataUrlOptions
{ {
/// <summary> /// <summary>
/// Gets or sets the scalefactor /// Gets or sets the scalefactor
/// </summary> /// </summary>
public float ScaleFactor { get; set; } = 1.0f; public float ScaleFactor { get; set; } = NativeImage.DefaultScaleFactor;
/// <summary>
/// Utility conversion for obsolete class
/// </summary>
public static implicit operator ImageOptions(ToDataUrlOptions o) => new () {ScaleFactor = o.ScaleFactor};
} }
} }

View File

@@ -1,13 +1,20 @@
namespace ElectronNET.API.Entities using System;
namespace ElectronNET.API.Entities
{ {
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
[Obsolete("Use ImageOptions instead.")]
public class ToPNGOptions public class ToPNGOptions
{ {
/// <summary> /// <summary>
/// Gets or sets the scalefactor /// Gets or sets the scalefactor
/// </summary> /// </summary>
public float ScaleFactor { get; set; } = 1.0f; public float ScaleFactor { get; set; } = 1.0f;
/// <summary>
/// Utility conversion for obsolete class
/// </summary>
public static implicit operator ImageOptions(ToPNGOptions o) => new () {ScaleFactor = o.ScaleFactor};
} }
} }