using System; using System.Collections.Generic; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Processing; using System.IO; using System.Linq; using System.Text.RegularExpressions; using Newtonsoft.Json; namespace ElectronNET.API.Entities { /// /// Native Image handler for Electron.NET /// [JsonConverter(typeof(NativeImageJsonConverter))] public class NativeImage { private readonly Dictionary _images = new(); private bool _isTemplateImage; private static readonly Dictionary ScaleFactorPairs = new() { {"@2x", 2.0f}, {"@3x", 3.0f}, {"@1x", 1.0f}, {"@4x", 4.0f}, {"@5x", 5.0f}, {"@1.25x", 1.25f}, {"@1.33x", 1.33f}, {"@1.4x", 1.4f}, {"@1.5x", 1.5f}, {"@1.8x", 1.8f}, {"@2.5x", 2.5f} }; private static float? ExtractDpiFromFilePath(string filePath) { var withoutExtension = Path.GetFileNameWithoutExtension(filePath); return ScaleFactorPairs .Where(p => withoutExtension.EndsWith(p.Key)) .Select(p => p.Value) .FirstOrDefault(); } private static Image BytesToImage(byte[] bytes) { return Image.Load(new MemoryStream(bytes)); } /// /// Creates an empty NativeImage /// public static NativeImage CreateEmpty() { return new NativeImage(); } /// /// /// public static NativeImage CreateFromImage(Image image, CreateFromBitmapOptions options = null) { if (options is null) { options = new CreateFromBitmapOptions(); } return new NativeImage(image, options.ScaleFactor); } /// /// Creates a NativeImage from a byte array. /// public static NativeImage CreateFromBuffer(byte[] buffer, CreateFromBufferOptions options = null) { if (options is null) { options = new CreateFromBufferOptions(); } var image = Image.Load(new MemoryStream(buffer)); return new NativeImage(image, options.ScaleFactor); } /// /// Creates a NativeImage from a base64 encoded data URL. /// /// A data URL with a base64 encoded image. public static NativeImage CreateFromDataURL(string dataUrl) { var images = new Dictionary(); var parsedDataUrl = Regex.Match(dataUrl, @"data:image/(?.+?),(?.+)"); var actualData = parsedDataUrl.Groups["data"].Value; var binData = Convert.FromBase64String(actualData); var image = BytesToImage(binData); images.Add(1.0f, image); return new NativeImage(images); } /// /// Creates a NativeImage from an image on the disk. /// /// The path of the image public static NativeImage CreateFromPath(string path) { var images = new Dictionary(); if (Regex.IsMatch(path, "(@.+?x)")) { var dpi = ExtractDpiFromFilePath(path); if (dpi == null) { throw new Exception($"Invalid scaling factor for '{path}'."); } images[dpi.Value] = Image.Load(path); } else { var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(path); var extension = Path.GetExtension(path); // Load as 1x dpi images[1.0f] = Image.Load(path); foreach (var scale in ScaleFactorPairs) { var fileName = $"{fileNameWithoutExtension}{scale}.{extension}"; if (File.Exists(fileName)) { var dpi = ExtractDpiFromFilePath(fileName); if (dpi != null) { images[dpi.Value] = Image.Load(fileName); } } } } return new NativeImage(images); } /// /// Creates an empty NativeImage /// public NativeImage() { } /// /// Creates a NativeImage from a bitmap and scale factor /// public NativeImage(Image bitmap, float scaleFactor = 1.0f) { _images.Add(scaleFactor, bitmap); } /// /// Creates a NativeImage from a dictionary of scales and images. /// public NativeImage(Dictionary imageDictionary) { _images = imageDictionary; } /// /// Crops the image specified by the input rectangle and computes scale factor /// public NativeImage Crop(Rectangle rect) { var images = new Dictionary(); foreach (var image in _images) { images.Add(image.Key, Crop(rect.X, rect.Y, rect.Width, rect.Height, image.Key)); } return new NativeImage(images); } /// /// Resizes the image and computes scale factor /// public NativeImage Resize(ResizeOptions options) { var images = new Dictionary(); foreach (var image in _images) { images.Add(image.Key, Resize(options.Width, options.Height, image.Key)); } return new NativeImage(images); } /// /// Add an image representation for a specific scale factor. /// /// public void AddRepresentation(AddRepresentationOptions options) { if (options.Buffer.Length > 0) { _images[options.ScaleFactor] = CreateFromBuffer(options.Buffer, new CreateFromBufferOptions {ScaleFactor = options.ScaleFactor}) .GetScale(options.ScaleFactor); } else if (!string.IsNullOrEmpty(options.DataUrl)) { _images[options.ScaleFactor] = CreateFromDataURL(options.DataUrl).GetScale(options.ScaleFactor); } } /// /// Gets the aspect ratio for the image based on scale factor /// /// Optional public float GetAspectRatio(float scaleFactor = 1.0f) { var image = GetScale(scaleFactor); if (image != null) { return (float)image.Width / image.Height; } return 0f; } /// /// Gets the size of the specified image based on scale factor /// public Size GetSize(float scaleFactor = 1.0f) { if (_images.ContainsKey(scaleFactor)) { var image = _images[scaleFactor]; return new Size { Width = image.Width, Height = image.Height }; } return null; } /// /// Checks to see if the NativeImage instance is empty. /// public bool IsEmpty() { return _images.Count <= 0; } /// /// Deprecated. Whether the image is a template image. /// public bool IsTemplateImage => _isTemplateImage; /// /// Deprecated. Marks the image as a template image. /// public void SetTemplateImage(bool option) { _isTemplateImage = option; } /// /// Outputs a bitmap based on the scale factor /// public MemoryStream ToBitmap(float scaleFactor = 1.0f) { var ms = new MemoryStream(); _images[scaleFactor].SaveAsBmp(ms); return ms; } /// /// Outputs a PNG based on the scale factor /// public MemoryStream ToPng(float scaleFactor = 1.0f) { var ms = new MemoryStream(); _images[scaleFactor].SaveAsPng(ms); return ms; } /// /// Outputs a JPEG for the default scale factor /// public MemoryStream ToJpeg(int quality, float scaleFactor = 1.0f) { var ms = new MemoryStream(); _images[scaleFactor].SaveAsJpeg(ms, new SixLabors.ImageSharp.Formats.Jpeg.JpegEncoder() { Quality = quality }); return ms; } /// /// Outputs a data URL based on the scale factor /// public string ToDataURL(float scaleFactor = 1.0f) { if (!_images.TryGetValue(scaleFactor, out var image)) { throw new KeyNotFoundException($"Missing scaleFactor = {scaleFactor}"); } return image.ToBase64String(PngFormat.Instance); } private Image Resize(int? width, int? height, float scaleFactor = 1.0f) { if (!_images.TryGetValue(scaleFactor, out var image)) { throw new KeyNotFoundException($"Missing scaleFactor = {scaleFactor}"); } if (width is null && height is null) { throw new ArgumentNullException("Missing width or height"); } 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); return image.Clone(c => c.Resize(new SixLabors.ImageSharp.Processing.ResizeOptions { Size = new(width.Value, height.Value), Sampler = KnownResamplers.Triangle, })); } private Image Crop(int? x, int? y, int? width, int? height, float scaleFactor = 1.0f) { if (!_images.TryGetValue(scaleFactor, out var image)) { throw new KeyNotFoundException($"Missing scaleFactor = {scaleFactor}"); } x ??= 0; y ??= 0; x = Convert.ToInt32(x * scaleFactor); y = Convert.ToInt32(y * scaleFactor); width ??= image.Width; height ??= image.Height; width = Convert.ToInt32(width * scaleFactor); height = Convert.ToInt32(height * scaleFactor); return image.Clone(c => c.Crop(new SixLabors.ImageSharp.Rectangle(x.Value, y.Value, width.Value, height.Value))); } internal Dictionary GetAllScaledImages() { var dict = new Dictionary(); try { foreach (var (scale, image) in _images) { dict.Add(scale, image.ToBase64String(PngFormat.Instance)); } } catch (Exception ex) { BridgeConnector.LogError(ex, "Error getting scaled images"); } return dict; } internal Image GetScale(float scaleFactor) { if (_images.TryGetValue(scaleFactor, out var image)) { return image; } return null; } } }