From 8971ff647062cd775b235dc89eca65cc80026667 Mon Sep 17 00:00:00 2001 From: dsalvia Date: Tue, 28 May 2024 10:47:45 -0300 Subject: [PATCH 01/38] fix test name --- test/Common/BitmapUnitTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Common/BitmapUnitTest.cs b/test/Common/BitmapUnitTest.cs index 7880c3f..9776689 100644 --- a/test/Common/BitmapUnitTest.cs +++ b/test/Common/BitmapUnitTest.cs @@ -92,7 +92,7 @@ public void Method_Clone() } [Test] - public void GetPixel_SetGetPixel() + public void Method_SetGetPixel() { using var bitmap = new Bitmap(100, 100); bitmap.SetPixel(50, 50, Color.Red); From 319815dbbccda5d11c84b7393bc3b7003120b971 Mon Sep 17 00:00:00 2001 From: dsalvia Date: Wed, 5 Jun 2024 15:07:18 -0300 Subject: [PATCH 02/38] redefine Image class based on SKBitmap and add missing props/methods; Image class will be the base class of Bitmap class --- src/Common/Image.cs | 247 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 195 insertions(+), 52 deletions(-) diff --git a/src/Common/Image.cs b/src/Common/Image.cs index 6b93915..37ca214 100644 --- a/src/Common/Image.cs +++ b/src/Common/Image.cs @@ -11,14 +11,14 @@ namespace GeneXus.Drawing; [Serializable] public class Image : IDisposable, ICloneable { - internal readonly SKImage m_image; - internal readonly SKCodec m_codec; + internal readonly SKBitmap m_bitmap; internal readonly ImageFormat m_format; + internal readonly int m_frames; - internal Image(SKImage skImage, SKData skData, ImageFormat format) + internal Image(SKBitmap bitmap, ImageFormat format, int frames) { - m_image = skImage; - m_codec = SKCodec.Create(skData); + m_bitmap = bitmap; + m_frames = frames; m_format = format; } @@ -38,11 +38,7 @@ internal Image(SKImage skImage, SKData skData, ImageFormat format) /// /// Cleans up resources for this . /// - public void Dispose() - { - m_image.Dispose(); - m_codec?.Dispose(); - } + public virtual void Dispose() => m_bitmap.Dispose(); #endregion @@ -52,13 +48,7 @@ public void Dispose() /// /// Creates an exact copy of this . /// - public virtual object Clone() - { - using var stream = new MemoryStream(); - Save(stream, RawFormat); - stream.Seek(0, SeekOrigin.Begin); - return FromStream(stream); - } + public virtual object Clone() => new Bitmap(m_bitmap.Copy()); #endregion @@ -68,7 +58,7 @@ public virtual object Clone() /// /// Creates a with the coordinates of the specified . /// - public static explicit operator SKImage(Image image) => image.m_image; + public static explicit operator SKImage(Image image) => SKImage.FromBitmap(image.m_bitmap); #endregion @@ -76,14 +66,53 @@ public virtual object Clone() #region Properties /// - /// Gets the width of this . + /// Gets attribute flags for the pixel data of this . /// - public int Width => m_image.Width; + public int Flags => throw new NotImplementedException(); + + /// + /// Gets an array of GUIDs that represent the dimensions of frames within this . + /// + public Guid[] FrameDimensionsList => throw new NotImplementedException(); /// /// Gets the height of this . /// - public int Height => m_image.Height; + public int Height => m_bitmap.Height; + + /// + /// Gets the horizontal resolution, in pixels-per-inch, of this . + /// + public float HorizontalResolution { get; protected set; } + + /// + /// Gets or sets the color palette used for this . + /// + public object Palette // TODO: implement ColorPalette + { + get => throw new NotImplementedException(); + set => throw new NotImplementedException(); + } + + /// + /// Gets the width and height of this . + /// + public Size PhysicalDimension => throw new NotImplementedException(); + + /// + /// Gets the for this . + /// + public object PixelFormat => throw new NotImplementedException(); + + /// + /// Gets IDs of the property items stored in this . + /// + public int[] PropertyIdList => throw new NotImplementedException(); + + /// + /// Gets all the property items (pieces of metadata) stored in this . + /// + public object[] PropertyItems => throw new NotImplementedException(); /// /// Gets the format of this . @@ -95,6 +124,21 @@ public virtual object Clone() /// public Size Size => new(Width, Height); + /// + /// Gets or sets an object that provides additional data about the image. + /// + public object Tag { get; set; } + + /// + /// Gets the vertical resolution, in pixels-per-inch, of this . + /// + public float VerticalResolution { get; protected set; } + + /// + /// Gets the width of this . + /// + public int Width => m_bitmap.Width; + #endregion @@ -109,6 +153,12 @@ public static Image FromFile(string filename) return FromStream(stream); } + /// + /// Creates a from a handle to a GDI bitmap. + /// + public static Bitmap FromHbitmap(IntPtr hbitmap) + => throw new NotSupportedException("windows specific"); + /// /// Creates an from the specified data stream. /// @@ -122,19 +172,21 @@ public static Image FromStream(Stream stream) var bounds = svg.Picture.CullRect; var size = new SKSizeI((int)bounds.Width, (int)bounds.Height); - var image = SKImage.FromPicture(svg.Picture, size); - return new Image(image, data, ImageFormat.Svg); + + var bitmap = SKBitmap.FromImage(image); + return new Image(bitmap, ImageFormat.Svg, 1); } else { var image = SKImage.FromEncodedData(data); var codec = SKCodec.Create(image.EncodedData); - if (!MAP_FORMAT.TryGetValue(codec.EncodedFormat, out ImageFormat format)) + if (!SK2GX.TryGetValue(codec.EncodedFormat, out var format)) throw new ArgumentException($"unsupported format {codec.EncodedFormat}"); - return new Image(image, data, format); + var bitmap = SKBitmap.FromImage(image); + return new Image(bitmap, format, Math.Max(codec.FrameCount, 1)); } } @@ -143,6 +195,72 @@ public static Image FromStream(Stream stream) #region Methods + /// + /// Gets a bounding rectangle in the specified units for this . + /// + public Rectangle GetBounds(ref object pageUnit) + => throw new NotImplementedException(); // TODO: implement GraphicsUnit + + /// + /// Returns information about the codecs used for this . + /// + public object GetEncoderParameterList(Guid encoder) + => throw new NotImplementedException(); // TODO: implement EncoderParameters + + /// + /// Returns the number of frames of the given dimension. + /// + public int GetFrameCount() + => m_frames; + + /// + /// Returns the size of the specified pixel format. + /// + public static int GetPixelFormatSize(object pixfmt) + => throw new NotImplementedException(); // TODO: implement PixelFormat + + /// + /// Gets the specified property item from this . + /// + public object GetPropertyItem(int propid) + => throw new NotImplementedException(); // TODO: implement PropertyItem + + /// + /// Returns the thumbnail for this . + /// + public Image GetThumbnailImage(int thumbWidth, int thumbHeight, object callback, IntPtr callbackData) + => throw new NotImplementedException(); + + /// + /// Returns a value indicating whether the pixel format contains alpha information. + /// + public static bool IsAlphaPixelFormat(object pixfmt) + => throw new NotImplementedException(); // TODO: implement PixelFormat + + /// + /// Returns a value indicating whether the pixel format is canonical. + /// + public static bool IsCanonicalPixelFormat(object pixfmt) + => throw new NotImplementedException(); // TODO: implement PixelFormat + + /// + /// Returns a value indicating whether the pixel format is extended. + /// + public static bool IsExtendedPixelFormat(object pixfmt) + => throw new NotImplementedException(); // TODO: implement PixelFormat + + /// + /// Removes the specified property item from this . + /// + public void RemovePropertyItem(int propid) + => throw new NotImplementedException(); + + /// + /// Rotates, flips, or rotates and flips the . + /// + public void RotateFlip(object rotateFlipType) + => throw new NotImplementedException(); // TODO: implement RotateFlipType + /// /// Saves this to the specified file. /// @@ -164,33 +282,51 @@ public void Save(string filename, ImageFormat format, int quality = 100) stream.Close(); } + // + /// Saves this to the specified file with the specified encoder and parameters. + /// + public void Save(string filename, object encoder, object encoderParams) // TODO: implement EncoderParameters + { + var file = new FileInfo(filename); + var stream = file.OpenWrite(); + Save(stream, encoder, encoderParams); + stream.Close(); + } + /// /// Saves this to the specified stream in the specified format. /// public void Save(Stream stream, ImageFormat format, int quality = 100) { - if (format == ImageFormat.Svg) + if (!GX2SK.TryGetValue(format, out var skFormat)) throw new NotSupportedException($"unsupported save {format} format"); - var skFormat = MAP_FORMAT.FirstOrDefault(kv => kv.Value == format).Key; - var bitmap = new SKBitmap(new SKImageInfo(Width, Height)); - - if (m_codec?.GetPixels(bitmap.Info, bitmap.GetPixels(), new SKCodecOptions(m_index)) != SKCodecResult.Success) - throw new Exception($"failed to decode frame {m_index}"); - - var image = SKImage.FromBitmap(bitmap); + var image = SKImage.FromBitmap(m_bitmap); // TODO: Only Png, Jpeg and Webp allowed to encode, otherwise returns null // ref: https://learn.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/bitmaps/saving#exploring-the-image-formats - var data = image.Encode(skFormat, quality) ?? throw new Exception($"unsupported format {format}"); + var data = image.Encode(skFormat, quality) ?? throw new NotSupportedException($"unsupported encoding format {format}"); data.SaveTo(stream); data.Dispose(); } /// - /// Returns the number of frames of the given dimension. + /// Saves this to the specified stream with the specified encoder and parameters. + /// + public void Save(Stream stream, object encoder, object encoderParams) + => throw new NotImplementedException(); // TODO: implement ImageCodecInfo & EncoderParameters + + /// + /// Adds an EncoderParameters to this . + /// + public void SaveAdd(object encoderParams) // TODO: implement EncoderParameters + => SaveAdd(this, encoderParams); + + /// + /// Adds an EncoderParameters to the specified . /// - public int GetFrameCount() => m_frames; + public void SaveAdd(Image image, object encoderParams) // TODO: implement EncoderParameters + => throw new NotImplementedException(); /// /// Selects the frame specified by the given dimension and index. @@ -206,6 +342,12 @@ public void SelectActiveFrame(int index) m_index = index; } + /// + /// Sets the specified property item to the specified value. + /// + public void SetPropertyItem(object propitem) + => throw new NotImplementedException(); // TODO: implement PropertyItem + #endregion @@ -213,24 +355,25 @@ public void SelectActiveFrame(int index) // Indexing for frame-based images (e.g. gif) internal int m_index = 0; - internal int m_frames => Math.Max(m_codec?.FrameCount ?? 0, 1); // Redefinition for image type - private static readonly Dictionary MAP_FORMAT = new() - { - { SKEncodedImageFormat.Bmp , ImageFormat.Bmp }, - { SKEncodedImageFormat.Gif , ImageFormat.Gif }, - { SKEncodedImageFormat.Ico , ImageFormat.Ico }, - { SKEncodedImageFormat.Png , ImageFormat.Png }, - { SKEncodedImageFormat.Wbmp, ImageFormat.Wbmp }, - { SKEncodedImageFormat.Webp, ImageFormat.Webp }, - { SKEncodedImageFormat.Pkm , ImageFormat.Pkm }, - { SKEncodedImageFormat.Ktx , ImageFormat.Ktx }, - { SKEncodedImageFormat.Astc, ImageFormat.Astc }, - { SKEncodedImageFormat.Dng , ImageFormat.Dng }, - { SKEncodedImageFormat.Heif, ImageFormat.Heif }, - { SKEncodedImageFormat.Jpeg, ImageFormat.Jpeg }, - }; + internal static readonly Dictionary SK2GX = new() + { + { SKEncodedImageFormat.Bmp , ImageFormat.Bmp }, + { SKEncodedImageFormat.Gif , ImageFormat.Gif }, + { SKEncodedImageFormat.Ico , ImageFormat.Ico }, + { SKEncodedImageFormat.Png , ImageFormat.Png }, + { SKEncodedImageFormat.Wbmp, ImageFormat.Wbmp }, + { SKEncodedImageFormat.Webp, ImageFormat.Webp }, + { SKEncodedImageFormat.Pkm , ImageFormat.Pkm }, + { SKEncodedImageFormat.Ktx , ImageFormat.Ktx }, + { SKEncodedImageFormat.Astc, ImageFormat.Astc }, + { SKEncodedImageFormat.Dng , ImageFormat.Dng }, + { SKEncodedImageFormat.Heif, ImageFormat.Heif }, + { SKEncodedImageFormat.Jpeg, ImageFormat.Jpeg }, + }; + + internal static readonly Dictionary GX2SK = SK2GX.ToDictionary(kv => kv.Value, kv => kv.Key); private static bool IsSvg(SKData data) { From 7f0f3e952b0b26b367657a2252213dc521ac63d0 Mon Sep 17 00:00:00 2001 From: dsalvia Date: Wed, 5 Jun 2024 15:13:47 -0300 Subject: [PATCH 03/38] make Bitmap inherit from Image class and add missing props/methods --- src/Common/Bitmap.cs | 202 +++++++++++++++++++++++++++++++------------ 1 file changed, 145 insertions(+), 57 deletions(-) diff --git a/src/Common/Bitmap.cs b/src/Common/Bitmap.cs index 7505e7d..17e21d9 100644 --- a/src/Common/Bitmap.cs +++ b/src/Common/Bitmap.cs @@ -6,26 +6,28 @@ namespace GeneXus.Drawing; [Serializable] -public class Bitmap : IDisposable, ICloneable +public class Bitmap : Image, IDisposable, ICloneable { - internal readonly SKBitmap m_bitmap; - internal Bitmap(SKBitmap skBitmap) - { - m_bitmap = skBitmap; - } + : base(skBitmap, ImageFormat.Png, 1) { } /// /// Initializes a new instance of the class from a filename /// - public Bitmap(string filename) - : this(Image.FromFile(filename)) { } + public Bitmap(string filename, bool useIcm = true) + : this(FromFile(filename)) { } /// /// Initializes a new instance of the class from a file stream /// - public Bitmap(Stream stream) - : this(Image.FromStream(stream)) { } + public Bitmap(Stream stream, bool useIcm = true) + : this(FromStream(stream)) { } + + /// + /// Initializes a new instance of the class from a file stream + /// + public Bitmap(Type type, string resource) + : this(GetResourceStream(type, resource)) { } /// /// Initializes a new instance of the class with the specified width and height @@ -33,6 +35,25 @@ public Bitmap(Stream stream) public Bitmap(float width, float height) : this(new SKBitmap((int)width, (int)height)) { } + /// + /// Initializes a new instance of the class with the specified size + /// and with the resolution of the specified object. + /// + //public Bitmap(int width, int height, object g) // TODO: Implement Graphics + // : this(width, height) => throw new NotImplementedException(); + + /// + /// Initializes a new instance of the class with the specified size and format. + /// + public Bitmap(int width, int height, object format) // TODO: Implement PixelFormat + : this(width, height, 4, format, IntPtr.Zero) { } + + /// + /// Initializes a new instance of the class with the specified size, pixel format, and pixel data + /// + public Bitmap(int width, int height, int stride, object format, IntPtr scan0) // TODO: Implement PixelFormat + : this(width, height) => throw new NotImplementedException(); + /// /// Initializes a new instance of the class with the specified /// @@ -43,31 +64,13 @@ public Bitmap(Size size) /// Initializes a new instance of the class with the specified /// public Bitmap(Image original) - : this(SKBitmap.FromImage(original.m_image)) - { - if (original.m_index == 0 && original.m_frames == 1) return; - - using var stream = new MemoryStream(); - original.Save(stream, ImageFormat.Png); - stream.Seek(0, SeekOrigin.Begin); - - var bitmap = m_bitmap; - - var image = Image.FromStream(stream); - m_bitmap = SKBitmap.FromImage(image.m_image); - - bitmap.Dispose(); - } + : this(original, original.Width, original.Height) { } /// /// Initializes a new instance of the class with the specified , width and height /// public Bitmap(Image original, float width, float height) - : this(original) - { - var resizeInfo = new SKImageInfo(Convert.ToInt32(width), Convert.ToInt32(height)); - m_bitmap = m_bitmap.Resize(resizeInfo, SKFilterQuality.High); - } + : this(original.m_bitmap.Resize(new SKImageInfo((int)width, (int)height), SKFilterQuality.High)) { } /// /// Initializes a new instance of the class with the specified and @@ -86,22 +89,18 @@ public Bitmap(Image original, Size size) public override string ToString() => $"{GetType().Name}: {Width}x{Height}"; - #region IDisposable - - /// - /// Cleans up resources for this . - /// - public void Dispose() => m_bitmap.Dispose(); - - #endregion - - #region IClonable /// - /// Creates an exact copy of this . + /// Creates a copy of the section of this defined + /// by structure and with a specified PixelFormat enumeration. /// - public object Clone() => new Bitmap(m_bitmap.Copy()); + public object Clone(Rectangle rect, object format) // TODO: Implement PixelFormat + { + var bitmap = new Bitmap(rect.Width, rect.Height); + var portion = new SKRectI((int)rect.Left, (int)rect.Top, (int)rect.Right, (int)rect.Bottom); + return m_bitmap.ExtractSubset(bitmap.m_bitmap, portion) ? bitmap : base.Clone(); + } #endregion @@ -109,7 +108,7 @@ public Bitmap(Image original, Size size) #region Operators /// - /// Creates a with the coordinates of the specified . + /// Creates a with the coordinates of the specified . /// public static explicit operator SKBitmap(Bitmap bitmap) => bitmap.m_bitmap; @@ -119,25 +118,56 @@ public Bitmap(Image original, Size size) #region Properties /// - /// Gets the width of this . + /// Gets the bytes-per-pixel value of this . /// - public int Width => m_bitmap.Width; + public int BytesPerPixel => m_bitmap.BytesPerPixel; /// - /// Gets the height of this . + /// Gets the handle of this . /// - public int Height => m_bitmap.Height; + public IntPtr Handle => m_bitmap.Handle; + + #endregion + + + #region Factory /// - /// Gets the bytes-per-pixel value of this . + /// Creates a from a Windows handle to an icon. /// - public int BytesPerPixel => m_bitmap.BytesPerPixel; + public static Bitmap FromHicon(IntPtr hicon) + => throw new NotSupportedException("windows specific"); + + /// + /// Creates a from the specified Windows resource. + /// + public static Bitmap FromResource(IntPtr hinstance, string bitmapName) + => throw new NotSupportedException("windows specific"); #endregion #region Methods + /// + /// Creates a GDI bitmap object from this . + /// + public IntPtr GetHbitmap() + => GetHbitmap(Color.LightGray); + + /// + /// Creates a GDI bitmap object from this with the + /// specificed structure as background. + /// + public IntPtr GetHbitmap(Color background) + => throw new NotSupportedException("windows specific"); + + /// + /// Returns the handle to an icon. + /// + public IntPtr GetHicon() + => throw new NotSupportedException("windows specific"); + /// /// Gets the color of the specified pixel in this . /// @@ -147,6 +177,38 @@ public Color GetPixel(int x, int y) return new Color(color.Alpha, color.Red, color.Green, color.Blue); } + /// + /// Locks a into system memory. + /// + public object LockBits(Rectangle rect, object flags, object format) // TODO: Implement ImageLockMode and PixelFormat + => LockBits(rect, flags, format, new()); + + /// + /// Locks a into system memory using a BitmapData information. + /// + public object LockBits(Rectangle rect, object flags, object format, object bitmapData) + => throw new NotImplementedException(); + + /// + /// Makes the default transparent color transparent for this . + /// + public void MakeTransparent() + => MakeTransparent(Width > 0 && Height > 0 ? GetPixel(0, Height - 1) : Color.LightGray); + + /// + /// Makes the specified color transparent for this . + /// + public void MakeTransparent(Color transparentColor) + { + if (transparentColor.A < 255) + return; // already transparent, return the bitmap as is + + for (int x = 0; x < Width; x++) + for (int y = 0; y < Height; y++) + if (GetPixel(x, y) == transparentColor) + SetPixel(x, y, Color.Translarent); + } + /// /// Sets the color of the specified pixel in this . /// @@ -156,20 +218,46 @@ public void SetPixel(int x, int y, Color color) m_bitmap.SetPixel(x, y, c); } - #endregion + /// + /// Sets the resolution for this . + /// + public void SetResolution(float xDpi, float yDpi) + { + HorizontalResolution = xDpi; // TODO: this is not doing anything + VerticalResolution = yDpi; + using var surface = SKSurface.Create(new SKImageInfo(Width, Height)); + float dpiX = 100f * surface.Canvas.DeviceClipBounds.Width / surface.Canvas.LocalClipBounds.Width; + float dpiY = 100f * surface.Canvas.DeviceClipBounds.Height / surface.Canvas.LocalClipBounds.Height; - #region Utilities + int width = (int)(Width * xDpi / dpiX); + int height = (int)(Height * yDpi / dpiY); + + using var resize = m_bitmap.Resize(new SKImageInfo(width, height), SKFilterQuality.High); + if (m_bitmap.ScalePixels(resize, SKFilterQuality.High)) + return; + throw new Exception("could not set resolution"); + } /// - /// Saves this into a stream with a specified format. + /// Unlocks this from system memory. /// - internal void Save(Stream stream, SKEncodedImageFormat format, int quality = 100) + public void UnlockBits(object bitmapdata) + => throw new NotImplementedException(); // TODO: implement BitmapData + + #endregion + + + #region Utilities + + private static Stream GetResourceStream(Type type, string resource) { - using var skStream = new SKManagedWStream(stream); - var isOk = m_bitmap.Encode(skStream, format, quality); - if (isOk) return; - throw new Exception("failed to save bitmap"); + if (type == null) + throw new ArgumentNullException(nameof(type)); + if (resource == null) + throw new ArgumentNullException(nameof(resource)); + return type.Module.Assembly.GetManifestResourceStream(type, resource) + ?? throw new ArgumentException("resource not found", nameof(resource)); } #endregion From d4282ca38fc0cf37bfdf61ef63c0a2fe137a1ffd Mon Sep 17 00:00:00 2001 From: dsalvia Date: Wed, 5 Jun 2024 15:17:02 -0300 Subject: [PATCH 04/38] init HorizontalResolution/VerticalResolution props with default resolution --- src/Common/Image.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Common/Image.cs b/src/Common/Image.cs index 37ca214..7f1edb4 100644 --- a/src/Common/Image.cs +++ b/src/Common/Image.cs @@ -20,6 +20,10 @@ internal Image(SKBitmap bitmap, ImageFormat format, int frames) m_bitmap = bitmap; m_frames = frames; m_format = format; + + using var surface = SKSurface.Create(new SKImageInfo(m_bitmap.Width, m_bitmap.Height)); + HorizontalResolution = (int)(100f * surface.Canvas.DeviceClipBounds.Width / surface.Canvas.LocalClipBounds.Width); + VerticalResolution = (int)(100f * surface.Canvas.DeviceClipBounds.Height / surface.Canvas.LocalClipBounds.Height); } /// @@ -66,7 +70,8 @@ internal Image(SKBitmap bitmap, ImageFormat format, int frames) #region Properties /// - /// Gets attribute flags for the pixel data of this . + /// Gets attribute flags for the pixel data of this defined by the + /// bitwise combination of . /// public int Flags => throw new NotImplementedException(); From 8da7bef97ad350dcbb968f3a4b403b093b3dc7a5 Mon Sep 17 00:00:00 2001 From: dsalvia Date: Wed, 5 Jun 2024 16:05:52 -0300 Subject: [PATCH 05/38] add missing props/methods in Icon class according to new defintiion of Bitmap class --- src/Common/Icon.cs | 67 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 61 insertions(+), 6 deletions(-) diff --git a/src/Common/Icon.cs b/src/Common/Icon.cs index bbc38d7..efee94a 100644 --- a/src/Common/Icon.cs +++ b/src/Common/Icon.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using GeneXus.Drawing.Imaging; using SkiaSharp; namespace GeneXus.Drawing; @@ -134,9 +135,9 @@ public object Clone() #region Properties /// - /// Gets the width of this . + /// Gets the Windows handle for this . This is not a copy of the handle; do not free it. /// - public int Width => m_entries[m_index].Width; + public IntPtr Handle => m_bitmap.Handle; /// /// Gets the height of this . @@ -148,20 +149,74 @@ public object Clone() /// public Size Size => new(Width, Height); + /// + /// Gets the width of this . + /// + public int Width => m_entries[m_index].Width; + + #endregion + + + #region Factory + + /// + /// Creates a GDI+ from the specified Windows handle to an icon (HICON). + /// + public static Icon FromHandle(IntPtr handle) + { + var info = new SKImageInfo(100, 100); + var skBitmap = new SKBitmap(); + skBitmap.InstallPixels(info, handle, info.RowBytes); + var bitmap = new Bitmap(skBitmap); + return new Icon(bitmap); + } + #endregion #region Methods /// - /// Converts this to a . + /// Returns an icon representation of an image that is contained in the specified file. + /// + public static Icon ExtractAssociatedIcon(string filePath) + => ExtractIcon(filePath, -1, true); // NOTE: https://stackoverflow.com/a/37419253 + + /// + /// Extracts a specified icon from the given . /// - public Bitmap ToBitmap() => new(m_Resized); + public static Icon ExtractIcon(string filePath, int id, bool smallIcon = false) + => ExtractIcon(filePath, id, smallIcon ? 8 : ushort.MaxValue); + + /// + /// Extracts a specified icon from the given and specified size. + /// + public static Icon ExtractIcon(string filePath, int id, int size) + { + if (size is <= 0 or > ushort.MaxValue) + throw new ArgumentOutOfRangeException(nameof(size)); + + if (new[] { ".dll", ".exe" }.Contains(Path.GetExtension(filePath))) + throw new ArgumentException("portable executable (PE) file format is not supported.", nameof(filePath)); + + var icon = new Icon(filePath, size, size); + if (id >= 0 && id < icon.m_entries.Count) // defined index + icon.m_index = id; + + return icon; + } /// /// Saves this to the specified output . /// - public void Save(Stream stream) => new Bitmap(m_Resized).Save(stream, SKEncodedImageFormat.Png, 100); + public void Save(Stream stream) + => new Bitmap(m_Resized).Save(stream, ImageFormat.Png, 100); + + /// + /// Converts this to a . + /// + public Bitmap ToBitmap() + => new(m_Resized); #endregion @@ -218,7 +273,7 @@ private SKBitmap m_Resized { get { - var original = m_bitmap.Clone() as Bitmap ?? throw new Exception("Cloning bitmap failed"); + var original = m_bitmap.Clone() as Bitmap ?? throw new Exception("cloning bitmap failed."); var resized = new SKBitmap(Width, Height); original.m_bitmap.ScalePixels(resized, SKFilterQuality.High); return resized; From 2aaa42f02f07d1e67fc9877f55038d920b74ce9a Mon Sep 17 00:00:00 2001 From: dsalvia Date: Wed, 5 Jun 2024 16:53:44 -0300 Subject: [PATCH 06/38] take the largest icon by default (and not the lowest) as System.Drawing does; fix constructor by copy --- src/Common/Icon.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Common/Icon.cs b/src/Common/Icon.cs index efee94a..a13f64e 100644 --- a/src/Common/Icon.cs +++ b/src/Common/Icon.cs @@ -39,13 +39,13 @@ private Icon(Bitmap bitmap, List entries, float width, float height) /// Initializes a new instance of the class from a instance. /// public Icon(Bitmap bitmap) - : this(bitmap, new List().Append(new IconEntry { Width = (byte)bitmap.Width, Height = (byte)bitmap.Height }).ToList(), int.MinValue, int.MinValue) { } + : this(bitmap, new() { new IconEntry { Width = (byte)bitmap.Width, Height = (byte)bitmap.Height } }, -1, -1) { } /// /// Initializes a new instance of the class from the specified file name. /// public Icon(string filename) - : this(filename, int.MinValue, int.MinValue) { } + : this(filename, byte.MaxValue, byte.MaxValue) { } /// /// Initializes a new instance of the class of the specified from the specified file. @@ -63,7 +63,7 @@ public Icon(string filename, float width, float height) /// Initializes a new instance of the class from the specified data stream. /// public Icon(Stream stream) - : this(stream, int.MinValue, int.MinValue) { } + : this(stream, byte.MaxValue, byte.MaxValue) { } /// /// Initializes a new instance of the class of the specified size from the specified stream. @@ -81,7 +81,7 @@ public Icon(Stream stream, float width, float height) /// Initializes a new instance of the class by copy. /// public Icon(Icon original) - : this(original, int.MinValue, int.MinValue) { } + : this(original, original.Width, original.Height) { } /// /// Initializes a new instance of the class by copy and attempts to find a version of the From eb2e37d3ebf1af67cbfe8ad0a9d0f99388e00bc7 Mon Sep 17 00:00:00 2001 From: dsalvia Date: Wed, 5 Jun 2024 16:54:44 -0300 Subject: [PATCH 07/38] add test for Bitmap.MakeTransparent method --- test/Common/BitmapUnitTest.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/Common/BitmapUnitTest.cs b/test/Common/BitmapUnitTest.cs index 9776689..614bc05 100644 --- a/test/Common/BitmapUnitTest.cs +++ b/test/Common/BitmapUnitTest.cs @@ -99,4 +99,20 @@ public void Method_SetGetPixel() var pixelColor = bitmap.GetPixel(50, 50); Assert.That(pixelColor, Is.EqualTo(Color.Red)); } + + [Test] + public void Method_MakeTransparent() + { + var filePath = Path.Combine(IMAGE_PATH, "Sample.png"); + using var bitmap = new Bitmap(filePath); + var color = Color.FromArgb(1, 77, 135); + bitmap.MakeTransparent(color); + Assert.Multiple(() => + { + for (int x = 0; x < bitmap.Width; x++) + for (int y = 0; y < bitmap.Height; y++) + if (bitmap.GetPixel(x, y) is Color pixel && pixel.R == color.R && pixel.G == color.G && pixel.B == color.B) + Assert.That(pixel.A, Is.Zero); + }); + } } From 4b159178c857a6ff5ef5975185e61827e5f537e8 Mon Sep 17 00:00:00 2001 From: dsalvia Date: Wed, 5 Jun 2024 17:02:12 -0300 Subject: [PATCH 08/38] fix Icon.ExtractAssociatedIcon to take the largest icon --- src/Common/Icon.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Common/Icon.cs b/src/Common/Icon.cs index a13f64e..33274b5 100644 --- a/src/Common/Icon.cs +++ b/src/Common/Icon.cs @@ -180,7 +180,7 @@ public static Icon FromHandle(IntPtr handle) /// Returns an icon representation of an image that is contained in the specified file. /// public static Icon ExtractAssociatedIcon(string filePath) - => ExtractIcon(filePath, -1, true); // NOTE: https://stackoverflow.com/a/37419253 + => ExtractIcon(filePath, -1); // NOTE: https://stackoverflow.com/a/37419253 /// /// Extracts a specified icon from the given . From fe02e352a4f92b828f77c294d8c7e44ca9445203 Mon Sep 17 00:00:00 2001 From: dsalvia Date: Wed, 5 Jun 2024 17:20:49 -0300 Subject: [PATCH 09/38] fix ExtractIcon to consider size prior to index --- src/Common/Icon.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Common/Icon.cs b/src/Common/Icon.cs index 33274b5..e9f85e5 100644 --- a/src/Common/Icon.cs +++ b/src/Common/Icon.cs @@ -200,8 +200,11 @@ public static Icon ExtractIcon(string filePath, int id, int size) throw new ArgumentException("portable executable (PE) file format is not supported.", nameof(filePath)); var icon = new Icon(filePath, size, size); - if (id >= 0 && id < icon.m_entries.Count) // defined index - icon.m_index = id; + if (icon.Width == size && icon.Height == size) + id = int.MaxValue; // set to undefined + + if (id >= 0 && id < icon.m_entries.Count) + icon.m_index = id; // set defined index return icon; } From f0b4b5e31551b47bb01b21c4dca79dfb4e1b5bb6 Mon Sep 17 00:00:00 2001 From: dsalvia Date: Wed, 5 Jun 2024 17:21:16 -0300 Subject: [PATCH 10/38] add ExtractIcon test cases --- test/Common/IconUnitTest.cs | 45 +++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/test/Common/IconUnitTest.cs b/test/Common/IconUnitTest.cs index 4d232e7..ab6fd44 100644 --- a/test/Common/IconUnitTest.cs +++ b/test/Common/IconUnitTest.cs @@ -141,4 +141,49 @@ public void Method_Save() byte[] pngHeader = new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }; Assert.That(streamHeader, Is.EqualTo(pngHeader)); } + + [Test] + public void Method_ExtractAssociatedIcon() + { + var filePath = Path.Combine(IMAGE_PATH, "Sample.ico"); + using var icon = Icon.ExtractAssociatedIcon(filePath); + Assert.Multiple(() => + { + Assert.That(icon, Is.Not.Null); + Assert.That(icon.Width, Is.EqualTo(32)); + Assert.That(icon.Height, Is.EqualTo(32)); + Assert.That(icon.Size.Width, Is.EqualTo(32)); + Assert.That(icon.Size.Height, Is.EqualTo(32)); + }); + } + + [Test] + public void Method_ExtractIcon_SmallIcon() + { + var filePath = Path.Combine(IMAGE_PATH, "Sample.ico"); + using var icon = Icon.ExtractIcon(filePath, 0, true); + Assert.Multiple(() => + { + Assert.That(icon, Is.Not.Null); + Assert.That(icon.Width, Is.EqualTo(16)); + Assert.That(icon.Height, Is.EqualTo(16)); + Assert.That(icon.Size.Width, Is.EqualTo(16)); + Assert.That(icon.Size.Height, Is.EqualTo(16)); + }); + } + + [Test] + public void Method_ExtractIcon_Size() + { + var filePath = Path.Combine(IMAGE_PATH, "Sample.ico"); + using var icon = Icon.ExtractIcon(filePath, 0, 20); + Assert.Multiple(() => + { + Assert.That(icon, Is.Not.Null); + Assert.That(icon.Width, Is.EqualTo(20)); + Assert.That(icon.Height, Is.EqualTo(20)); + Assert.That(icon.Size.Width, Is.EqualTo(20)); + Assert.That(icon.Size.Height, Is.EqualTo(20)); + }); + } } From 48660bd95fcd2c21aa1030d5f8401c6af72dd439 Mon Sep 17 00:00:00 2001 From: dsalvia Date: Thu, 6 Jun 2024 09:13:56 -0300 Subject: [PATCH 11/38] implement Image.GetBounds, add GraphicsUnit enum --- src/Common/GraphicsUnit.cs | 42 ++++++++++++++++++++++++++++++++++++++ src/Common/Image.cs | 10 ++++++--- 2 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 src/Common/GraphicsUnit.cs diff --git a/src/Common/GraphicsUnit.cs b/src/Common/GraphicsUnit.cs new file mode 100644 index 0000000..b8189e9 --- /dev/null +++ b/src/Common/GraphicsUnit.cs @@ -0,0 +1,42 @@ +namespace GeneXus.Drawing; + +/// +/// Specifies the unit of measure for the given data. +/// +public enum GraphicsUnit +{ + /// + /// Specifies the world unit as the unit of measure. + /// + World = 0, + + /// + /// Specifies 1/75 inch as the unit of measure. + /// + Display = 1, + + /// + /// Specifies a device pixel as the unit of measure. + /// + Pixel = 2, + + /// + /// Specifies a printer's point (1/72 inch) as the unit of measure. + /// + Point = 3, + + /// + /// Specifies the inch as the unit of measure. + /// + Inch = 4, + + /// + /// Specifies the document unit (1/300 inch) as the unit of measure. + /// + Document = 5, + + /// + /// Specifies the millimeter as the unit of measure. + /// + Millimeter = 6 +} diff --git a/src/Common/Image.cs b/src/Common/Image.cs index 7f1edb4..a0bc34a 100644 --- a/src/Common/Image.cs +++ b/src/Common/Image.cs @@ -93,7 +93,7 @@ internal Image(SKBitmap bitmap, ImageFormat format, int frames) /// /// Gets or sets the color palette used for this . /// - public object Palette // TODO: implement ColorPalette + public object Palette { get => throw new NotImplementedException(); set => throw new NotImplementedException(); @@ -203,8 +203,12 @@ public static Image FromStream(Stream stream) /// /// Gets a bounding rectangle in the specified units for this . /// - public Rectangle GetBounds(ref object pageUnit) - => throw new NotImplementedException(); // TODO: implement GraphicsUnit + public Rectangle GetBounds(ref GraphicsUnit pageUnit) + { + var bounds = new Rectangle(0, 0, Width, Height); + pageUnit = GraphicsUnit.Pixel; + return bounds; + } /// /// Returns information about the codecs used for this . From 7ae345ad93eb241eec352aa62fa0e313fbbcc7eb Mon Sep 17 00:00:00 2001 From: dsalvia Date: Thu, 6 Jun 2024 09:15:17 -0300 Subject: [PATCH 12/38] fix typo in Color.Transparent --- src/Common/Bitmap.cs | 2 +- src/Common/Color.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Common/Bitmap.cs b/src/Common/Bitmap.cs index 17e21d9..c00069c 100644 --- a/src/Common/Bitmap.cs +++ b/src/Common/Bitmap.cs @@ -206,7 +206,7 @@ public void MakeTransparent(Color transparentColor) for (int x = 0; x < Width; x++) for (int y = 0; y < Height; y++) if (GetPixel(x, y) == transparentColor) - SetPixel(x, y, Color.Translarent); + SetPixel(x, y, Color.Transparent); } /// diff --git a/src/Common/Color.cs b/src/Common/Color.cs index eee5bf8..672b990 100644 --- a/src/Common/Color.cs +++ b/src/Common/Color.cs @@ -301,7 +301,7 @@ public static string KnownColorToName(Color color) #region KnownColors - public static Color Translarent => new(SKColors.Transparent); + public static Color Transparent => new(SKColors.Transparent); public static Color AliceBlue => new(SKColors.AliceBlue); public static Color AntiqueWhite => new(SKColors.AntiqueWhite); public static Color Aqua => new(SKColors.Aqua); From 6645619396d52350d1a7108b43da7113f6fb1ac5 Mon Sep 17 00:00:00 2001 From: dsalvia Date: Thu, 6 Jun 2024 09:21:39 -0300 Subject: [PATCH 13/38] add ImageLockMode enum --- src/Common/Bitmap.cs | 8 ++++---- src/Common/Imaging/ImageLockMode.cs | 27 +++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 src/Common/Imaging/ImageLockMode.cs diff --git a/src/Common/Bitmap.cs b/src/Common/Bitmap.cs index c00069c..55aa1c4 100644 --- a/src/Common/Bitmap.cs +++ b/src/Common/Bitmap.cs @@ -180,14 +180,14 @@ public Color GetPixel(int x, int y) /// /// Locks a into system memory. /// - public object LockBits(Rectangle rect, object flags, object format) // TODO: Implement ImageLockMode and PixelFormat - => LockBits(rect, flags, format, new()); + public object LockBits(Rectangle rect, ImageLockMode flags, object format) + => LockBits(rect, flags, format, new()); // TODO: implement BitmapData /// /// Locks a into system memory using a BitmapData information. /// - public object LockBits(Rectangle rect, object flags, object format, object bitmapData) - => throw new NotImplementedException(); + public object LockBits(Rectangle rect, ImageLockMode flags, object format, object bitmapData) + => throw new NotImplementedException(); // TODO: implement BitmapData /// /// Makes the default transparent color transparent for this . diff --git a/src/Common/Imaging/ImageLockMode.cs b/src/Common/Imaging/ImageLockMode.cs new file mode 100644 index 0000000..dbf7551 --- /dev/null +++ b/src/Common/Imaging/ImageLockMode.cs @@ -0,0 +1,27 @@ +namespace GeneXus.Drawing.Imaging; + +/// +/// Indicates the access mode for an . +/// +public enum ImageLockMode +{ + /// + /// Specifies the image is read-only. + /// + ReadOnly = 1, + + /// + /// Specifies the image is write-only. + /// + WriteOnly = 2, + + /// + /// Specifies the image is read-write. + /// + ReadWrite = ReadOnly | WriteOnly, + + /// + /// Indicates the image resides in a user input buffer, to which the user controls access. + /// + UserInputBuffer = 4 +} From b6dbcb4d7c215f0fcd185611ebea282e74e3a689 Mon Sep 17 00:00:00 2001 From: dsalvia Date: Thu, 6 Jun 2024 15:20:16 -0300 Subject: [PATCH 14/38] add PixelFormat enum; implement PixelFormat in Bitmap/Image classes --- src/Common/Bitmap.cs | 10 +-- src/Common/Image.cs | 71 ++++++++++++++-- src/Common/Imaging/PixelFormat.cs | 135 ++++++++++++++++++++++++++++++ 3 files changed, 202 insertions(+), 14 deletions(-) create mode 100644 src/Common/Imaging/PixelFormat.cs diff --git a/src/Common/Bitmap.cs b/src/Common/Bitmap.cs index 55aa1c4..e0d8f10 100644 --- a/src/Common/Bitmap.cs +++ b/src/Common/Bitmap.cs @@ -45,13 +45,13 @@ public Bitmap(float width, float height) /// /// Initializes a new instance of the class with the specified size and format. /// - public Bitmap(int width, int height, object format) // TODO: Implement PixelFormat + public Bitmap(int width, int height, PixelFormat format) : this(width, height, 4, format, IntPtr.Zero) { } /// /// Initializes a new instance of the class with the specified size, pixel format, and pixel data /// - public Bitmap(int width, int height, int stride, object format, IntPtr scan0) // TODO: Implement PixelFormat + public Bitmap(int width, int height, int stride, PixelFormat format, IntPtr scan0) : this(width, height) => throw new NotImplementedException(); /// @@ -95,7 +95,7 @@ public Bitmap(Image original, Size size) /// Creates a copy of the section of this defined /// by structure and with a specified PixelFormat enumeration. /// - public object Clone(Rectangle rect, object format) // TODO: Implement PixelFormat + public object Clone(Rectangle rect, PixelFormat format) { var bitmap = new Bitmap(rect.Width, rect.Height); var portion = new SKRectI((int)rect.Left, (int)rect.Top, (int)rect.Right, (int)rect.Bottom); @@ -180,13 +180,13 @@ public Color GetPixel(int x, int y) /// /// Locks a into system memory. /// - public object LockBits(Rectangle rect, ImageLockMode flags, object format) + public object LockBits(Rectangle rect, ImageLockMode flags, PixelFormat format) => LockBits(rect, flags, format, new()); // TODO: implement BitmapData /// /// Locks a into system memory using a BitmapData information. /// - public object LockBits(Rectangle rect, ImageLockMode flags, object format, object bitmapData) + public object LockBits(Rectangle rect, ImageLockMode flags, PixelFormat format, object bitmapData) => throw new NotImplementedException(); // TODO: implement BitmapData /// diff --git a/src/Common/Image.cs b/src/Common/Image.cs index a0bc34a..b6779a6 100644 --- a/src/Common/Image.cs +++ b/src/Common/Image.cs @@ -107,7 +107,7 @@ public object Palette /// /// Gets the for this . /// - public object PixelFormat => throw new NotImplementedException(); + public PixelFormat PixelFormat => ToPixelFormat(m_bitmap.ColorType, m_bitmap.AlphaType, m_bitmap.BytesPerPixel, m_bitmap.Pixels, m_bitmap.Bytes); /// /// Gets IDs of the property items stored in this . @@ -225,8 +225,8 @@ public int GetFrameCount() /// /// Returns the size of the specified pixel format. /// - public static int GetPixelFormatSize(object pixfmt) - => throw new NotImplementedException(); // TODO: implement PixelFormat + public static int GetPixelFormatSize(PixelFormat pixfmt) + => ((int)pixfmt >> 8) & 0xFF; /// /// Gets the specified property item from this . @@ -243,20 +243,20 @@ public Image GetThumbnailImage(int thumbWidth, int thumbHeight, object callback, /// /// Returns a value indicating whether the pixel format contains alpha information. /// - public static bool IsAlphaPixelFormat(object pixfmt) - => throw new NotImplementedException(); // TODO: implement PixelFormat + public static bool IsAlphaPixelFormat(PixelFormat pixfmt) + => (pixfmt & PixelFormat.Alpha) != 0; /// /// Returns a value indicating whether the pixel format is canonical. /// - public static bool IsCanonicalPixelFormat(object pixfmt) - => throw new NotImplementedException(); // TODO: implement PixelFormat + public static bool IsCanonicalPixelFormat(PixelFormat pixfmt) + => (pixfmt & PixelFormat.Canonical) != 0; /// /// Returns a value indicating whether the pixel format is extended. /// - public static bool IsExtendedPixelFormat(object pixfmt) - => throw new NotImplementedException(); // TODO: implement PixelFormat + public static bool IsExtendedPixelFormat(PixelFormat pixfmt) + => (pixfmt & PixelFormat.Extended) != 0; /// /// Removes the specified property item from this . @@ -391,5 +391,58 @@ private static bool IsSvg(SKData data) return Encoding.UTF8.GetString(header) == " pixel.Red == pixel.Green && pixel.Green == pixel.Blue)) + return PixelFormat.Format8bppIndexed; // NOTE: to behave as System.Drawing but SkiaSharp seems to treat it differently + + switch (colorType) + { + case SKColorType.Rgb565 when alphaType == SKAlphaType.Opaque: + return bytesPerPixel == 2 ? PixelFormat.Format16bppRgb565 + : PixelFormat.Undefined; + + case SKColorType.Rgb888x when alphaType == SKAlphaType.Unpremul: + return bytesPerPixel == 3 ? PixelFormat.Format24bppRgb + : bytesPerPixel == 4 ? PixelFormat.Format32bppRgb + : bytesPerPixel == 6 ? PixelFormat.Format48bppRgb + : PixelFormat.Undefined; + + case SKColorType.Rgba8888 when alphaType == SKAlphaType.Opaque: + case SKColorType.Bgra8888 when alphaType == SKAlphaType.Opaque: + return bytesPerPixel == 4 ? PixelFormat.Format24bppRgb + : bytesPerPixel == 6 ? PixelFormat.Format32bppRgb + : bytesPerPixel == 8 ? PixelFormat.Format48bppRgb + : PixelFormat.Undefined; + + case SKColorType.Rgba8888 when alphaType == SKAlphaType.Unpremul: + case SKColorType.Bgra8888 when alphaType == SKAlphaType.Unpremul: + return bytesPerPixel == 4 ? PixelFormat.Format32bppArgb + : bytesPerPixel == 8 ? PixelFormat.Format64bppArgb + : PixelFormat.Undefined; + + case SKColorType.Rgba8888 when alphaType == SKAlphaType.Premul: + case SKColorType.Bgra8888 when alphaType == SKAlphaType.Premul: + return bytesPerPixel == 4 ? PixelFormat.Format32bppPArgb + : bytesPerPixel == 8 ? PixelFormat.Format64bppPArgb + : PixelFormat.Undefined; + + case SKColorType.Gray8: + return bytesPerPixel == 2 ? PixelFormat.Format16bppGrayScale + : PixelFormat.Undefined; + + default: + /* + * TODO: missing value mappings + * - Format1bppIndexed + * - Format4bppIndexed + * - Format8bppIndexed + * - Format16bppRgb555 + * - Format16bppArgb1555 + */ + return PixelFormat.Undefined; + } + } + #endregion } diff --git a/src/Common/Imaging/PixelFormat.cs b/src/Common/Imaging/PixelFormat.cs new file mode 100644 index 0000000..b00b02b --- /dev/null +++ b/src/Common/Imaging/PixelFormat.cs @@ -0,0 +1,135 @@ +namespace GeneXus.Drawing.Imaging; + +/// +/// Specifies the format of the color data for each pixel in the image. +/// +public enum PixelFormat +{ + /// + /// Specifies that pixel data contains color indexed values which means they are an index to colors in the + /// system color table, as opposed to individual color values. + /// + Indexed = 0x00010000, + + /// + /// Specifies that pixel data contains GDI colors. + /// + Gdi = 0x00020000, + + /// + /// Specifies that pixel data contains alpha values that are not pre-multiplied. + /// + Alpha = 0x00040000, + + /// + /// Specifies that pixel format contains pre-multiplied alpha values. + /// + PAlpha = 0x00080000, + + /// + /// Specifies that pixel format contains extended color values of 16 bits per channel. + /// + Extended = 0x00100000, + + /// + /// The default pixel format of 32 bits per pixel. The format specifies 24-bit color depth and an 8-bit alpha channel. + /// + Canonical = 0x00200000, + + /// + /// Specifies that pixel format is undefined. + /// + Undefined = 0, + + /// + /// Specifies that pixel format doesn't matter. + /// + DontCare = Undefined, + + /// + /// Specifies that pixel format is 1 bit per pixel indexed color. The color table therefore has two colors in it. + /// + Format1bppIndexed = 1 | (1 << 8) | Indexed | Gdi, + + /// + /// Specifies that pixel format is 4 bits per pixel indexed color. The color table therefore has 16 colors in it. + /// + Format4bppIndexed = 2 | (4 << 8) | Indexed | Gdi, + + /// + /// Specifies that pixel format is 8 bits per pixel indexed color. The color table therefore has 256 colors in it. + /// + Format8bppIndexed = 3 | (8 << 8) | Indexed | Gdi, + + /// + /// Specifies that pixel format is 16 bits per pixel. The color information specifies 65536 shades of gray. + /// + Format16bppGrayScale = 4 | (16 << 8) | Extended, + + /// + /// Specifies that pixel format is 16 bits per pixel. The color information specifies 32768 shades of color of + /// which 5 bits are red, 5 bits are green and 5 bits are blue. + /// + Format16bppRgb555 = 5 | (16 << 8) | Gdi, + + /// + /// Specifies that pixel format is 16 bits per pixel. The color information specifies 32768 shades of color of + /// which 5 bits are red, 6 bits are green and 5 bits are blue. + /// + Format16bppRgb565 = 6 | (16 << 8) | Gdi, + + /// + /// Specifies that pixel format is 16 bits per pixel. The color information specifies 32768 shades of color of + /// which 5 bits are red, 5 bits are green, 5 bits are blue and 1 bit is alpha. + /// + Format16bppArgb1555 = 7 | (16 << 8) | Alpha | Gdi, + + /// + /// Specifies that pixel format is 24 bits per pixel. The color information specifies 16777216 shades of color + /// of which 8 bits are red, 8 bits are green and 8 bits are blue. + /// + Format24bppRgb = 8 | (24 << 8) | Gdi, + + /// + /// Specifies that pixel format is 24 bits per pixel. The color information specifies 16777216 shades of color + /// of which 8 bits are red, 8 bits are green and 8 bits are blue. + /// + Format32bppRgb = 9 | (32 << 8) | Gdi, + + /// + /// Specifies that pixel format is 32 bits per pixel. The color information specifies 16777216 shades of color + /// of which 8 bits are red, 8 bits are green and 8 bits are blue. The 8 additional bits are alpha bits. + /// + Format32bppArgb = 10 | (32 << 8) | Alpha | Gdi | Canonical, + + /// + /// Specifies that pixel format is 32 bits per pixel. The color information specifies 16777216 shades of color + /// of which 8 bits are red, 8 bits are green and 8 bits are blue. The 8 additional bits are pre-multiplied alpha bits. + /// + Format32bppPArgb = 11 | (32 << 8) | Alpha | PAlpha | Gdi, + + /// + /// Specifies that pixel format is 48 bits per pixel. The color information specifies 16777216 shades of color + /// of which 8 bits are red, 8 bits are green and 8 bits are blue. The 8 additional bits are alpha bits. + /// + Format48bppRgb = 12 | (48 << 8) | Extended, + + /// + /// Specifies pixel format is 64 bits per pixel. The color information specifies 16777216 shades of color of + /// which 16 bits are red, 16 bits are green and 16 bits are blue. The 16 additional bits are alpha bits. + /// + Format64bppArgb = 13 | (64 << 8) | Alpha | Canonical | Extended, + + /// + /// Specifies that pixel format is 64 bits per pixel. The color information specifies 16777216 shades of color + /// of which 16 bits are red, 16 bits are green and 16 bits are blue. The 16 additional bits are pre-multiplied + /// alpha bits. + /// + Format64bppPArgb = 14 | (64 << 8) | Alpha | PAlpha | Extended, + + /// + /// Specifies that pixel format is 64 bits per pixel. The color information specifies 16777216 shades of color + /// of which 16 bits are red, 16 bits are green and 16 bits are blue. The 16 additional bits are alpha bits. + /// + Max = 15 +} From c1827be3ec214d4b6122f8efc214915f476821cb Mon Sep 17 00:00:00 2001 From: dsalvia Date: Thu, 6 Jun 2024 18:05:56 -0300 Subject: [PATCH 15/38] add RotateFlipType enum, implement Image.RotateFlip method --- src/Common/Image.cs | 25 ++++++++++- src/Common/RotateFlipType.cs | 87 ++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 src/Common/RotateFlipType.cs diff --git a/src/Common/Image.cs b/src/Common/Image.cs index b6779a6..12538ca 100644 --- a/src/Common/Image.cs +++ b/src/Common/Image.cs @@ -267,8 +267,29 @@ public void RemovePropertyItem(int propid) /// /// Rotates, flips, or rotates and flips the . /// - public void RotateFlip(object rotateFlipType) - => throw new NotImplementedException(); // TODO: implement RotateFlipType + public void RotateFlip(RotateFlipType rotateFlipType) + { + (int degrees, int scaleX, int scaleY) = rotateFlipType switch + { + RotateFlipType.RotateNoneFlipNone => (0, 1, 1), + RotateFlipType.RotateNoneFlipX => (0, -1, 1), + RotateFlipType.RotateNoneFlipY => (0, 1, -1), + RotateFlipType.RotateNoneFlipXY => (0, -1, -1), + RotateFlipType.Rotate90FlipNone => (90, 1, 1), + RotateFlipType.Rotate90FlipX => (90, 1, -1), + RotateFlipType.Rotate90FlipY => (90, -1, 1), + RotateFlipType.Rotate90FlipXY => (90, -1, -1), + _ => throw new ArgumentException($"unrecognized value {rotateFlipType}", nameof(rotateFlipType)) + }; + + using var surface = SKSurface.Create(m_bitmap.Info); + surface.Canvas.RotateDegrees(degrees, Width / 2f, Height / 2f); + surface.Canvas.Scale(scaleX, scaleY, Width / 2f, Height / 2f); + surface.Canvas.DrawBitmap(m_bitmap, 0, 0); + + var rotated = SKBitmap.FromImage(surface.Snapshot()); + m_bitmap.SetPixels(rotated.GetPixels()); + } /// /// Saves this to the specified file. diff --git a/src/Common/RotateFlipType.cs b/src/Common/RotateFlipType.cs new file mode 100644 index 0000000..48e21c8 --- /dev/null +++ b/src/Common/RotateFlipType.cs @@ -0,0 +1,87 @@ +namespace GeneXus.Drawing; + +/// +/// Specifies how much an image is rotated and the axis used to flip the image. +/// +public enum RotateFlipType +{ + /// + /// Specifies no clockwise rotation and no flipping. + /// + RotateNoneFlipNone = 0, + + /// + /// Specifies a 90-degree clockwise rotation without flipping. + /// + Rotate90FlipNone = 1, + + /// + /// Specifies a 180-degree clockwise rotation without flipping + /// + Rotate180FlipNone = 2, + + /// + /// Specifies a 270-degree clockwise rotation without flipping. + /// + Rotate270FlipNone = 3, + + /// + /// Specifies no clockwise rotation followed by a horizontal flip. + /// + RotateNoneFlipX = 4, + + /// + /// Specifies a 90-degree clockwise rotation followed by a horizontal flip. + /// + Rotate90FlipX = 5, + + /// + /// Specifies a 180-degree clockwise rotation followed by a horizontal flip. + /// + Rotate180FlipX = 6, + + /// + /// Specifies a 270-degree clockwise rotation followed by a horizontal flip. + /// + Rotate270FlipX = 7, + + /// + /// Specifies no clockwise rotation followed by a vertical flip. + /// + RotateNoneFlipY = Rotate180FlipX, + + /// + /// Specifies a 90-degree clockwise rotation followed by a vertical flip. + /// + Rotate90FlipY = Rotate270FlipX, + + /// + /// Specifies a 180-degree clockwise rotation followed by a vertical flip. + /// + Rotate180FlipY = RotateNoneFlipX, + + /// + /// Specifies a 270-degree clockwise rotation followed by a vertical flip. + /// + Rotate270FlipY = Rotate90FlipX, + + /// + /// Specifies no clockwise rotation followed by a horizontal and vertical flip. + /// + RotateNoneFlipXY = Rotate180FlipNone, + + /// + /// Specifies a 90-degree clockwise rotation followed by a horizontal and vertical flip + /// + Rotate90FlipXY = Rotate270FlipNone, + + /// + /// Specifies a 180-degree clockwise rotation followed by a horizontal and vertical flip. + /// + Rotate180FlipXY = RotateNoneFlipNone, + + /// + /// Specifies a 270-degree clockwise rotation followed by a horizontal and vertical flip. + /// + Rotate270FlipXY = Rotate90FlipNone +} From 5b98308023baa33366a2670075006f0d337b4f1a Mon Sep 17 00:00:00 2001 From: dsalvia Date: Fri, 7 Jun 2024 10:08:45 -0300 Subject: [PATCH 16/38] remove unused parameter from ToPixelFormat --- src/Common/Image.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Common/Image.cs b/src/Common/Image.cs index 12538ca..0095119 100644 --- a/src/Common/Image.cs +++ b/src/Common/Image.cs @@ -107,7 +107,7 @@ public object Palette /// /// Gets the for this . /// - public PixelFormat PixelFormat => ToPixelFormat(m_bitmap.ColorType, m_bitmap.AlphaType, m_bitmap.BytesPerPixel, m_bitmap.Pixels, m_bitmap.Bytes); + public PixelFormat PixelFormat => ToPixelFormat(m_bitmap.ColorType, m_bitmap.AlphaType, m_bitmap.BytesPerPixel, m_bitmap.Pixels); /// /// Gets IDs of the property items stored in this . @@ -412,7 +412,7 @@ private static bool IsSvg(SKData data) return Encoding.UTF8.GetString(header) == " pixel.Red == pixel.Green && pixel.Green == pixel.Blue)) return PixelFormat.Format8bppIndexed; // NOTE: to behave as System.Drawing but SkiaSharp seems to treat it differently From ac7e3f90b22898c806b3c4b30c8ebd626dbc78b5 Mon Sep 17 00:00:00 2001 From: dsalvia Date: Fri, 7 Jun 2024 10:19:12 -0300 Subject: [PATCH 17/38] add ImageFlags enum, add Image.Flags basic implementation --- src/Common/Image.cs | 47 ++++++++++++++++++- src/Common/Imaging/ImageFlags.cs | 80 ++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 src/Common/Imaging/ImageFlags.cs diff --git a/src/Common/Image.cs b/src/Common/Image.cs index 0095119..153c593 100644 --- a/src/Common/Image.cs +++ b/src/Common/Image.cs @@ -73,7 +73,52 @@ internal Image(SKBitmap bitmap, ImageFormat format, int frames) /// Gets attribute flags for the pixel data of this defined by the /// bitwise combination of . /// - public int Flags => throw new NotImplementedException(); + public int Flags + { + get + { + var flags = ImageFlags.None; + + if (m_bitmap.IsImmutable) + flags |= ImageFlags.ReadOnly; + + if (m_bitmap.Pixels.Any(pixel => pixel.Alpha < 255)) + flags |= ImageFlags.HasAlpha; + + if (m_bitmap.Pixels.Any(pixel => pixel.Alpha > 0 && pixel.Alpha < 255)) + flags |= ImageFlags.HasTranslucent; + + if (m_bitmap.Width > 0 && m_bitmap.Height > 0) + flags |= ImageFlags.HasRealPixelSize; + + switch (m_bitmap.ColorType) + { + case SKColorType.Rgb565: + case SKColorType.Rgb888x: + case SKColorType.Rgba8888: + case SKColorType.Bgra8888: + flags |= ImageFlags.ColorSpaceRgb; + break; + + case SKColorType.Gray8: + flags |= ImageFlags.ColorSpaceGray; + break; + } + + /* + * TODO: Missing flags + * - ImageFlags.Caching + * - ImageFlags.ColorSpaceCmyk + * - ImageFlags.ColorSpaceYcbcr + * - ImageFlags.ColorSpaceYcck + * - ImageFlags.HasRealDpi + * - ImageFlags.PartiallyScalable + * - ImageFlags.Scalable + */ + + return (int)flags; + } + } /// /// Gets an array of GUIDs that represent the dimensions of frames within this . diff --git a/src/Common/Imaging/ImageFlags.cs b/src/Common/Imaging/ImageFlags.cs new file mode 100644 index 0000000..73e9dd2 --- /dev/null +++ b/src/Common/Imaging/ImageFlags.cs @@ -0,0 +1,80 @@ +using System; + +namespace GeneXus.Drawing.Imaging; + +[Flags] +/// +/// Specifies the attributes of the pixel data contained in an object. +/// +public enum ImageFlags +{ + /// + /// There is no format information. + /// + None = 0, + + /// + /// Pixel data is scalable. + /// + Scalable = 0x0001, + + /// + /// Pixel data contains alpha information. + /// + HasAlpha = 0x0002, + + /// + /// Pixel data has alpha values other than 0 (transparent) and 255 (opaque). + /// + HasTranslucent = 0x0004, + + /// + /// Pixel data is partially scalable, but there are some limitations. + /// + PartiallyScalable = 0x0008, + + /// + /// Pixel data uses an RGB color space. + /// + ColorSpaceRgb = 0x0010, + + /// + /// Pixel data uses a CMYK color space. + /// + ColorSpaceCmyk = 0x0020, + + /// + /// Pixel data is grayscale. + /// + ColorSpaceGray = 0x0040, + + /// + /// Pixel data is stored using a YCBCR color space. + /// + ColorSpaceYcbcr = 0x0080, + + /// + /// Pixel data is stored using a YCCK color space. + /// + ColorSpaceYcck = 0x0100, + + /// + /// Specifies that dots per inch information is stored in the image. + /// + HasRealDpi = 0x1000, + + /// + /// Specifies that the pixel size is stored in the image. + /// + HasRealPixelSize = 0x2000, + + /// + /// The pixel data is read-only. + /// + ReadOnly = 0x00010000, + + /// + /// The pixel data can be cached for faster access. + /// + Caching = 0x00020000 +} From 672fa7880d4c887fdaa4282361e123b3467d88ba Mon Sep 17 00:00:00 2001 From: dsalvia Date: Fri, 7 Jun 2024 10:21:20 -0300 Subject: [PATCH 18/38] minor fix on PixelFormat property's summary --- src/Common/Image.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Common/Image.cs b/src/Common/Image.cs index 153c593..2536491 100644 --- a/src/Common/Image.cs +++ b/src/Common/Image.cs @@ -150,7 +150,7 @@ public object Palette public Size PhysicalDimension => throw new NotImplementedException(); /// - /// Gets the for this . + /// Gets the for this . /// public PixelFormat PixelFormat => ToPixelFormat(m_bitmap.ColorType, m_bitmap.AlphaType, m_bitmap.BytesPerPixel, m_bitmap.Pixels); From 87ec06ec649033805db0a37115c11543180aca46 Mon Sep 17 00:00:00 2001 From: dsalvia Date: Fri, 7 Jun 2024 11:08:51 -0300 Subject: [PATCH 19/38] mark Bitmap.SetResolution as not supported --- src/Common/Bitmap.cs | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/Common/Bitmap.cs b/src/Common/Bitmap.cs index e0d8f10..c0ae7c7 100644 --- a/src/Common/Bitmap.cs +++ b/src/Common/Bitmap.cs @@ -223,20 +223,9 @@ public void SetPixel(int x, int y, Color color) /// public void SetResolution(float xDpi, float yDpi) { - HorizontalResolution = xDpi; // TODO: this is not doing anything - VerticalResolution = yDpi; - - using var surface = SKSurface.Create(new SKImageInfo(Width, Height)); - float dpiX = 100f * surface.Canvas.DeviceClipBounds.Width / surface.Canvas.LocalClipBounds.Width; - float dpiY = 100f * surface.Canvas.DeviceClipBounds.Height / surface.Canvas.LocalClipBounds.Height; - - int width = (int)(Width * xDpi / dpiX); - int height = (int)(Height * yDpi / dpiY); - - using var resize = m_bitmap.Resize(new SKImageInfo(width, height), SKFilterQuality.High); - if (m_bitmap.ScalePixels(resize, SKFilterQuality.High)) - return; - throw new Exception("could not set resolution"); + if (xDpi <= 0 || yDpi <= 0) + throw new ArgumentOutOfRangeException("DPI values must be greater than zero."); + throw new NotSupportedException("not supported by skia"); // SOURCE: https://issues.skia.org/issues/40043604 } /// From dea319e28ae0a5d3ff6ffdcb9b7d993d78eaf2c3 Mon Sep 17 00:00:00 2001 From: dsalvia Date: Fri, 7 Jun 2024 11:51:13 -0300 Subject: [PATCH 20/38] add Image.GetThumbnailImage --- src/Common/Image.cs | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/Common/Image.cs b/src/Common/Image.cs index 2536491..04da21b 100644 --- a/src/Common/Image.cs +++ b/src/Common/Image.cs @@ -67,6 +67,17 @@ internal Image(SKBitmap bitmap, ImageFormat format, int frames) #endregion + #region Delegates + + /// + /// Provides a callback method for determining when the method + /// should prematurely cancel execution. + /// + public delegate bool GetThumbnailImageAbort(); + + #endregion + + #region Properties /// @@ -278,12 +289,18 @@ public static int GetPixelFormatSize(PixelFormat pixfmt) /// public object GetPropertyItem(int propid) => throw new NotImplementedException(); // TODO: implement PropertyItem - + /// /// Returns the thumbnail for this . /// - public Image GetThumbnailImage(int thumbWidth, int thumbHeight, object callback, IntPtr callbackData) - => throw new NotImplementedException(); + public Image GetThumbnailImage(int thumbWidth, int thumbHeight, GetThumbnailImageAbort callback, IntPtr callbackData) + { + // NOTE: callback and callbackData parameters are ignored according to the deocumentation + var info = new SKImageInfo(thumbWidth, thumbHeight, m_bitmap.ColorType, m_bitmap.AlphaType, m_bitmap.ColorSpace); + var thumb = m_bitmap.Resize(info, SKFilterQuality.High); + if (thumb == null) throw new Exception("could not resize."); + return new Image(thumb, m_format, m_frames); + } /// /// Returns a value indicating whether the pixel format contains alpha information. From 2ff98f7890d20078c137de112c77f8a783fb361f Mon Sep 17 00:00:00 2001 From: dsalvia Date: Fri, 7 Jun 2024 14:30:53 -0300 Subject: [PATCH 21/38] add ColorPalette class, implement Image.Palette property --- src/Common/Image.cs | 14 ++-- src/Common/Imaging/ColorPalette.cs | 111 +++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 5 deletions(-) create mode 100644 src/Common/Imaging/ColorPalette.cs diff --git a/src/Common/Image.cs b/src/Common/Image.cs index 04da21b..8fb77f0 100644 --- a/src/Common/Image.cs +++ b/src/Common/Image.cs @@ -15,12 +15,16 @@ public class Image : IDisposable, ICloneable internal readonly ImageFormat m_format; internal readonly int m_frames; + private ColorPalette m_palette; + internal Image(SKBitmap bitmap, ImageFormat format, int frames) { m_bitmap = bitmap; m_frames = frames; m_format = format; + m_palette = new ColorPalette(); + using var surface = SKSurface.Create(new SKImageInfo(m_bitmap.Width, m_bitmap.Height)); HorizontalResolution = (int)(100f * surface.Canvas.DeviceClipBounds.Width / surface.Canvas.LocalClipBounds.Width); VerticalResolution = (int)(100f * surface.Canvas.DeviceClipBounds.Height / surface.Canvas.LocalClipBounds.Height); @@ -74,7 +78,7 @@ internal Image(SKBitmap bitmap, ImageFormat format, int frames) /// should prematurely cancel execution. /// public delegate bool GetThumbnailImageAbort(); - + #endregion @@ -149,10 +153,10 @@ public int Flags /// /// Gets or sets the color palette used for this . /// - public object Palette + public ColorPalette Palette { - get => throw new NotImplementedException(); - set => throw new NotImplementedException(); + get => m_palette; + set => m_palette = value.Entries.Length > 0 ? value : throw new ArgumentException("parameter is not valid."); } /// @@ -289,7 +293,7 @@ public static int GetPixelFormatSize(PixelFormat pixfmt) /// public object GetPropertyItem(int propid) => throw new NotImplementedException(); // TODO: implement PropertyItem - + /// /// Returns the thumbnail for this . /// diff --git a/src/Common/Imaging/ColorPalette.cs b/src/Common/Imaging/ColorPalette.cs new file mode 100644 index 0000000..d4814e4 --- /dev/null +++ b/src/Common/Imaging/ColorPalette.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace GeneXus.Drawing.Imaging; + +public sealed class ColorPalette +{ + private readonly int m_flags; + private readonly Color[] m_entries; + + internal ColorPalette(int flags, Color[] entries) + { + m_flags = flags; + m_entries = entries; + } + + /// + /// Create a custom color palette. + /// + /// Color entries for the palette. + public ColorPalette(params Color[] colors) + : this(0, colors) { } + + /// + /// Create an optimal color palette based on the colors in a given bitmap. + /// + public static ColorPalette CreateOptimalPalette(int colors, bool useTransparentColor, Bitmap bitmap) + { + var entries = QuantizeColors(bitmap, colors, useTransparentColor); + int flags = 0x0; + if (HasAlphaInfo(entries)) + flags |= 0x1; + if (IsGrayScaled(entries)) + flags |= 0x2; + if (IsHalftone(entries)) + flags |= 0x4; + return new ColorPalette(flags, entries); + } + + + #region Properties + + /// + /// Specifies how to interpret the color information in the array of colors. + /// - 0x01: The color values in the array contain alpha information. + /// - 0x02: The colors in the array are grayscale values. + /// - 0x04: The colors in the array are halftone values. + /// + public int Flags => m_flags; + + /// + /// Specifies an array of objects. + /// + public Color[] Entries => m_entries; + + #endregion + + + #region Utilities + + private static Color[] QuantizeColors(Bitmap bitmap, int count, bool useTransparentColor) + { + var pixels = new List(); + for (int x = 0; x < bitmap.Width; x++) + for (int y = 0; y < bitmap.Height; y++) + pixels.Add(bitmap.GetPixel(x, y)); + return pixels // median cut cuantization + .Where(c => useTransparentColor || c != Color.Transparent) + .GroupBy(c => new { c.R, c.G, c.B }) + .OrderByDescending(g => g.Count()) + .Take(count) + .Select(g => g.First()) + .ToArray(); + } + + private static bool HasAlphaInfo(Color[] colors) + => colors.Any(c => c.A > 255); + + private static bool IsGrayScaled(Color[] colors) + => colors.All(c => c.R == c.G && c.G == c.B); + + private static bool IsHalftone(Color[] colors) + { + const float DOT_SIZE_VAR_THRESHOLD = 0.2f; + const float DOT_SPACING_VAR_THRESHOLD = 0.2f; + + for (int i = 1; i < colors.Length; i++) + { + var currColor = colors[i]; + var prevColor = colors[i - 1]; + + var currDotSize = currColor.GetBrightness(); + var prevDotSize = prevColor.GetBrightness(); + var dotSizeVariation = Math.Abs(currDotSize - prevDotSize) / Math.Max(currDotSize, prevDotSize); + if (dotSizeVariation > DOT_SIZE_VAR_THRESHOLD) + return false; + + var currDotSpacing = 1 - currColor.GetBrightness(); + var prevDotSpacing = 1 - prevColor.GetBrightness(); + var dotSpacingVariation = Math.Abs(currDotSpacing - prevDotSpacing) / Math.Max(currDotSpacing, prevDotSpacing); + if (dotSpacingVariation > DOT_SPACING_VAR_THRESHOLD) + return false; + } + + return true; + } + + #endregion +} From 3d2b4c279d34d703ff20189bd5c626a577330b38 Mon Sep 17 00:00:00 2001 From: dsalvia Date: Mon, 10 Jun 2024 09:22:43 -0300 Subject: [PATCH 22/38] minor improvement --- src/Common/Image.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Common/Image.cs b/src/Common/Image.cs index 8fb77f0..cf9d627 100644 --- a/src/Common/Image.cs +++ b/src/Common/Image.cs @@ -302,8 +302,9 @@ public Image GetThumbnailImage(int thumbWidth, int thumbHeight, GetThumbnailImag // NOTE: callback and callbackData parameters are ignored according to the deocumentation var info = new SKImageInfo(thumbWidth, thumbHeight, m_bitmap.ColorType, m_bitmap.AlphaType, m_bitmap.ColorSpace); var thumb = m_bitmap.Resize(info, SKFilterQuality.High); - if (thumb == null) throw new Exception("could not resize."); - return new Image(thumb, m_format, m_frames); + return thumb == null + ? throw new Exception("could not resize.") + : new Image(thumb, m_format, m_frames); } /// From 68a4bf1e8fed276fb7943e5419c27ae6abb826e3 Mon Sep 17 00:00:00 2001 From: dsalvia Date: Mon, 10 Jun 2024 12:57:50 -0300 Subject: [PATCH 23/38] add image test cases --- test/Common/ImageUnitTest.cs | 76 ++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/test/Common/ImageUnitTest.cs b/test/Common/ImageUnitTest.cs index 4da0af2..071a73c 100644 --- a/test/Common/ImageUnitTest.cs +++ b/test/Common/ImageUnitTest.cs @@ -135,4 +135,80 @@ public void Method_SelectActiveFrame() using var bitmap = new Bitmap(image); Assert.That(bitmap.GetPixel(40, 10), Is.EqualTo(Color.Red)); } + + [Test] + [TestCase(RotateFlipType.RotateNoneFlipNone, 0, 1, 1)] + [TestCase(RotateFlipType.RotateNoneFlipX, 0, -1, 1)] + [TestCase(RotateFlipType.RotateNoneFlipY, 0, 1, -1)] + [TestCase(RotateFlipType.RotateNoneFlipXY, 0, -1, -1)] + [TestCase(RotateFlipType.Rotate90FlipNone, 90, 1, 1)] + [TestCase(RotateFlipType.Rotate90FlipX, 90, -1, 1)] + [TestCase(RotateFlipType.Rotate90FlipY, 90, 1, -1)] + [TestCase(RotateFlipType.Rotate90FlipXY, 90, -1, -1)] + public void Method_RotateFlip(RotateFlipType type, int deg, int sx, int sy) + { + var filePath = Path.Combine(IMAGE_PATH, "Sample.png"); + using var image = Image.FromFile(filePath); + + using var rotated = image.Clone() as Image; + rotated.RotateFlip(type); + + (int pivW, int pivH) = (image.Width, image.Height); + (int newW, int newH) = deg switch // calculate size after rotation and flipping + { + 0 or 180 => (image.Width, image.Height), + 90 or 270 => (image.Height, image.Width), + _ => throw new ArgumentException($"unrecognized angle {deg}", nameof(deg)) + }; + + Assert.Multiple(() => + { + Assert.That(rotated.Width, Is.EqualTo(newW)); + Assert.That(rotated.Height, Is.EqualTo(newH)); + }); + + (int pivX, int pivY) = (17, 23); + (int newX, int newY) = deg switch // calculate coordinates after rotation and flipping + { + 0 => (sx == -1 ? pivW - 1 - pivX : pivX, sy == -1 ? pivH - 1 - pivY : pivY), + 90 => (sx == -1 ? pivY : pivW - 1 - pivY, sy == -1 ? pivH - 1 - pivX : pivX), + 180 => (sx == -1 ? pivX : pivW - 1 - pivX, sy == -1 ? pivY : pivH - 1 - pivY), + 270 => (sx == -1 ? pivH - 1 - pivY : pivY, sy == -1 ? pivX : pivW - 1 - pivX), + _ => throw new ArgumentException($"unrecognized angle {deg}", nameof(deg)) + }; + + using var expected = new Bitmap(image); + using var obtained = new Bitmap(rotated); + Assert.That(obtained.GetPixel(newX, newY), Is.EqualTo(expected.GetPixel(pivX, pivY))); + } + + [Test] + public void Method_GetThumbnailImage() + { + var filePath = Path.Combine(IMAGE_PATH, "Sample.png"); + using var image = Image.FromFile(filePath); + using var thumb = image.GetThumbnailImage(20, 20, new Image.GetThumbnailImageAbort(() => false), IntPtr.Zero); + Assert.Multiple(() => + { + Assert.That(thumb.Width, Is.EqualTo(20)); + Assert.That(thumb.Height, Is.EqualTo(20)); + }); + } + + [Test] + public void Method_GetBounds() + { + var filePath = Path.Combine(IMAGE_PATH, "Sample.png"); + using var image = Image.FromFile(filePath); + var unit = GraphicsUnit.Pixel; + var bounds = image.GetBounds(ref unit); + Assert.Multiple(() => + { + Assert.That(unit, Is.EqualTo(GraphicsUnit.Pixel)); + Assert.That(bounds.X, Is.Zero); + Assert.That(bounds.Y, Is.Zero); + Assert.That(bounds.Width, Is.EqualTo(image.Width)); + Assert.That(bounds.Height, Is.EqualTo(image.Height)); + }); + } } From f50653be33d3c01a437124efe4df5b2f8ad685cd Mon Sep 17 00:00:00 2001 From: dsalvia Date: Mon, 17 Jun 2024 09:53:38 -0300 Subject: [PATCH 24/38] fix Dispose pattern --- src/Common/Bitmap.cs | 2 +- src/Common/Icon.cs | 10 ++++++++-- src/Common/Image.cs | 10 ++++++++-- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/Common/Bitmap.cs b/src/Common/Bitmap.cs index c0ae7c7..527055b 100644 --- a/src/Common/Bitmap.cs +++ b/src/Common/Bitmap.cs @@ -81,7 +81,7 @@ public Bitmap(Image original, Size size) /// /// Cleans up resources for this . /// - ~Bitmap() => Dispose(); + ~Bitmap() => Dispose(false); /// /// Creates a human-readable string that represents this . diff --git a/src/Common/Icon.cs b/src/Common/Icon.cs index e9f85e5..2b2b8aa 100644 --- a/src/Common/Icon.cs +++ b/src/Common/Icon.cs @@ -100,7 +100,7 @@ public Icon(Icon original, float width, float height) /// /// Cleans up resources for this . /// - ~Icon() => Dispose(); + ~Icon() => Dispose(false); /// /// Creates a human-readable string that represents this . @@ -113,7 +113,13 @@ public Icon(Icon original, float width, float height) /// /// Cleans up resources for this . /// - public void Dispose() => m_bitmap.Dispose(); + public void Dispose() + { + GC.SuppressFinalize(this); + Dispose(true); + } + + private void Dispose(bool disposing) => m_bitmap.Dispose(); #endregion diff --git a/src/Common/Image.cs b/src/Common/Image.cs index cf9d627..397522c 100644 --- a/src/Common/Image.cs +++ b/src/Common/Image.cs @@ -33,7 +33,7 @@ internal Image(SKBitmap bitmap, ImageFormat format, int frames) /// /// Cleans up resources for this . /// - ~Image() => Dispose(); + ~Image() => Dispose(false); /// /// Creates a human-readable string that represents this . @@ -46,7 +46,13 @@ internal Image(SKBitmap bitmap, ImageFormat format, int frames) /// /// Cleans up resources for this . /// - public virtual void Dispose() => m_bitmap.Dispose(); + public void Dispose() + { + GC.SuppressFinalize(this); + Dispose(true); + } + + protected virtual void Dispose(bool disposing) => m_bitmap.Dispose(); #endregion From 222406d5dedc9e17a81b02557f428e4fd6aa5e95 Mon Sep 17 00:00:00 2001 From: dsalvia Date: Mon, 17 Jun 2024 09:54:30 -0300 Subject: [PATCH 25/38] make Bitmap/Icon sealed --- src/Common/Bitmap.cs | 2 +- src/Common/Icon.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Common/Bitmap.cs b/src/Common/Bitmap.cs index 527055b..c484300 100644 --- a/src/Common/Bitmap.cs +++ b/src/Common/Bitmap.cs @@ -6,7 +6,7 @@ namespace GeneXus.Drawing; [Serializable] -public class Bitmap : Image, IDisposable, ICloneable +public sealed class Bitmap : Image, IDisposable, ICloneable { internal Bitmap(SKBitmap skBitmap) : base(skBitmap, ImageFormat.Png, 1) { } diff --git a/src/Common/Icon.cs b/src/Common/Icon.cs index 2b2b8aa..fc77ac1 100644 --- a/src/Common/Icon.cs +++ b/src/Common/Icon.cs @@ -8,7 +8,7 @@ namespace GeneXus.Drawing; [Serializable] -public class Icon : IDisposable, ICloneable +public sealed class Icon : IDisposable, ICloneable { internal readonly Bitmap m_bitmap; internal readonly List m_entries; From c94a88e6dfb24b4c6d8d9c715461e285185f0133 Mon Sep 17 00:00:00 2001 From: Fede Azzato Date: Mon, 17 Jun 2024 11:20:58 -0300 Subject: [PATCH 26/38] Fix typo. --- src/Common/Image.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Common/Image.cs b/src/Common/Image.cs index 397522c..5d2d0da 100644 --- a/src/Common/Image.cs +++ b/src/Common/Image.cs @@ -305,7 +305,7 @@ public object GetPropertyItem(int propid) /// public Image GetThumbnailImage(int thumbWidth, int thumbHeight, GetThumbnailImageAbort callback, IntPtr callbackData) { - // NOTE: callback and callbackData parameters are ignored according to the deocumentation + // NOTE: callback and callbackData parameters are ignored according to the documentation var info = new SKImageInfo(thumbWidth, thumbHeight, m_bitmap.ColorType, m_bitmap.AlphaType, m_bitmap.ColorSpace); var thumb = m_bitmap.Resize(info, SKFilterQuality.High); return thumb == null From 6cf0b8cfb2c2de0b3b3197b8f23f8a06a223a156 Mon Sep 17 00:00:00 2001 From: Fede Azzato Date: Mon, 17 Jun 2024 12:30:01 -0300 Subject: [PATCH 27/38] Fix summary open tag declaration --- src/Common/Image.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Common/Image.cs b/src/Common/Image.cs index 5d2d0da..884fea4 100644 --- a/src/Common/Image.cs +++ b/src/Common/Image.cs @@ -385,7 +385,7 @@ public void Save(string filename, ImageFormat format, int quality = 100) stream.Close(); } - // + /// /// Saves this to the specified file with the specified encoder and parameters. /// public void Save(string filename, object encoder, object encoderParams) // TODO: implement EncoderParameters From ec4c06e2b90d4b2b3284950d90bb78d2a012457f Mon Sep 17 00:00:00 2001 From: Fede Azzato Date: Mon, 17 Jun 2024 14:49:51 -0300 Subject: [PATCH 28/38] Rename properties to match naming convention. --- src/Common/Icon.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Common/Icon.cs b/src/Common/Icon.cs index fc77ac1..b817722 100644 --- a/src/Common/Icon.cs +++ b/src/Common/Icon.cs @@ -12,7 +12,7 @@ public sealed class Icon : IDisposable, ICloneable { internal readonly Bitmap m_bitmap; internal readonly List m_entries; - internal int m_index { get; private set; } = 0; + internal int EntryIndex { get; private set; } private Icon(Bitmap bitmap, List entries, float width, float height) { @@ -31,7 +31,7 @@ private Icon(Bitmap bitmap, List entries, float width, float height) if (diff >= pivot) continue; pivot = diff; - m_index = i; + EntryIndex = i; } } @@ -148,7 +148,7 @@ public object Clone() /// /// Gets the height of this . /// - public int Height => m_entries[m_index].Height; + public int Height => m_entries[EntryIndex].Height; /// /// Gets the size of this . @@ -158,7 +158,7 @@ public object Clone() /// /// Gets the width of this . /// - public int Width => m_entries[m_index].Width; + public int Width => m_entries[EntryIndex].Width; #endregion @@ -210,7 +210,7 @@ public static Icon ExtractIcon(string filePath, int id, int size) id = int.MaxValue; // set to undefined if (id >= 0 && id < icon.m_entries.Count) - icon.m_index = id; // set defined index + icon.EntryIndex = id; // set defined index return icon; } @@ -219,13 +219,13 @@ public static Icon ExtractIcon(string filePath, int id, int size) /// Saves this to the specified output . /// public void Save(Stream stream) - => new Bitmap(m_Resized).Save(stream, ImageFormat.Png, 100); + => new Bitmap(Resized).Save(stream, ImageFormat.Png, 100); /// /// Converts this to a . /// public Bitmap ToBitmap() - => new(m_Resized); + => new(Resized); #endregion @@ -278,7 +278,7 @@ private static List ReadIco(Stream stream) return entries; } - private SKBitmap m_Resized + private SKBitmap Resized { get { From 0c962dcebb4a675949fb99e0761b68f99eacfac0 Mon Sep 17 00:00:00 2001 From: dsalvia Date: Mon, 17 Jun 2024 17:10:58 -0300 Subject: [PATCH 29/38] define Image as abstract --- src/Common/Bitmap.cs | 4 ++-- src/Common/Image.cs | 14 ++++++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/Common/Bitmap.cs b/src/Common/Bitmap.cs index c484300..cb802e6 100644 --- a/src/Common/Bitmap.cs +++ b/src/Common/Bitmap.cs @@ -8,8 +8,8 @@ namespace GeneXus.Drawing; [Serializable] public sealed class Bitmap : Image, IDisposable, ICloneable { - internal Bitmap(SKBitmap skBitmap) - : base(skBitmap, ImageFormat.Png, 1) { } + internal Bitmap(SKBitmap bitmap, ImageFormat format = ImageFormat.Png, int frames = 1) + : base(bitmap, format, frames) { } /// /// Initializes a new instance of the class from a filename diff --git a/src/Common/Image.cs b/src/Common/Image.cs index 884fea4..7611c69 100644 --- a/src/Common/Image.cs +++ b/src/Common/Image.cs @@ -9,7 +9,7 @@ namespace GeneXus.Drawing; [Serializable] -public class Image : IDisposable, ICloneable +public abstract class Image : IDisposable, ICloneable { internal readonly SKBitmap m_bitmap; internal readonly ImageFormat m_format; @@ -246,7 +246,7 @@ public static Image FromStream(Stream stream) var image = SKImage.FromPicture(svg.Picture, size); var bitmap = SKBitmap.FromImage(image); - return new Image(bitmap, ImageFormat.Svg, 1); + return CreateInstance(bitmap, ImageFormat.Svg, 1); } else { @@ -257,7 +257,7 @@ public static Image FromStream(Stream stream) throw new ArgumentException($"unsupported format {codec.EncodedFormat}"); var bitmap = SKBitmap.FromImage(image); - return new Image(bitmap, format, Math.Max(codec.FrameCount, 1)); + return CreateInstance(bitmap, format, Math.Max(codec.FrameCount, 1)); } } @@ -310,7 +310,7 @@ public Image GetThumbnailImage(int thumbWidth, int thumbHeight, GetThumbnailImag var thumb = m_bitmap.Resize(info, SKFilterQuality.High); return thumb == null ? throw new Exception("could not resize.") - : new Image(thumb, m_format, m_frames); + : CreateInstance(thumb, m_format, m_frames); } /// @@ -456,6 +456,12 @@ public void SetPropertyItem(object propitem) #region Utilities + private static Image CreateInstance(SKBitmap bitmap, ImageFormat format, int frames) + { + using var surface = SKSurface.Create(new SKImageInfo(bitmap.Width, bitmap.Height)); + return new Bitmap(bitmap, format, frames); + } + // Indexing for frame-based images (e.g. gif) internal int m_index = 0; From 41b7f0995aaa6bee51ba540851f4c8e8a385d9bd Mon Sep 17 00:00:00 2001 From: dsalvia Date: Mon, 17 Jun 2024 17:12:54 -0300 Subject: [PATCH 30/38] remove Bitmap destructor (aleady defined in Image class) --- src/Common/Bitmap.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Common/Bitmap.cs b/src/Common/Bitmap.cs index cb802e6..41d4293 100644 --- a/src/Common/Bitmap.cs +++ b/src/Common/Bitmap.cs @@ -78,11 +78,6 @@ public Bitmap(Image original, float width, float height) public Bitmap(Image original, Size size) : this(original, size.Width, size.Height) { } - /// - /// Cleans up resources for this . - /// - ~Bitmap() => Dispose(false); - /// /// Creates a human-readable string that represents this . /// From a117fc60b8e40e4b1ba8bca3f67222c6868aed19 Mon Sep 17 00:00:00 2001 From: dsalvia Date: Mon, 17 Jun 2024 17:15:15 -0300 Subject: [PATCH 31/38] remove CreateInstance method (just use Bitmap constructor) --- src/Common/Image.cs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/Common/Image.cs b/src/Common/Image.cs index 7611c69..f52aa71 100644 --- a/src/Common/Image.cs +++ b/src/Common/Image.cs @@ -246,7 +246,7 @@ public static Image FromStream(Stream stream) var image = SKImage.FromPicture(svg.Picture, size); var bitmap = SKBitmap.FromImage(image); - return CreateInstance(bitmap, ImageFormat.Svg, 1); + return new Bitmap(bitmap, ImageFormat.Svg, 1); } else { @@ -257,7 +257,7 @@ public static Image FromStream(Stream stream) throw new ArgumentException($"unsupported format {codec.EncodedFormat}"); var bitmap = SKBitmap.FromImage(image); - return CreateInstance(bitmap, format, Math.Max(codec.FrameCount, 1)); + return new Bitmap(bitmap, format, Math.Max(codec.FrameCount, 1)); } } @@ -310,7 +310,7 @@ public Image GetThumbnailImage(int thumbWidth, int thumbHeight, GetThumbnailImag var thumb = m_bitmap.Resize(info, SKFilterQuality.High); return thumb == null ? throw new Exception("could not resize.") - : CreateInstance(thumb, m_format, m_frames); + : new Bitmap(thumb, m_format, m_frames); } /// @@ -456,12 +456,6 @@ public void SetPropertyItem(object propitem) #region Utilities - private static Image CreateInstance(SKBitmap bitmap, ImageFormat format, int frames) - { - using var surface = SKSurface.Create(new SKImageInfo(bitmap.Width, bitmap.Height)); - return new Bitmap(bitmap, format, frames); - } - // Indexing for frame-based images (e.g. gif) internal int m_index = 0; From a5e125ee921b10491408e953dcc31e7b21549689 Mon Sep 17 00:00:00 2001 From: dsalvia Date: Mon, 17 Jun 2024 17:22:12 -0300 Subject: [PATCH 32/38] add using directive for temp Image instances --- src/Common/Image.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Common/Image.cs b/src/Common/Image.cs index f52aa71..5bfc8b5 100644 --- a/src/Common/Image.cs +++ b/src/Common/Image.cs @@ -240,17 +240,17 @@ public static Image FromStream(Stream stream) { var svg = new SkiaSharp.Extended.Svg.SKSvg(); svg.Load(data.AsStream()); - + var bounds = svg.Picture.CullRect; var size = new SKSizeI((int)bounds.Width, (int)bounds.Height); - var image = SKImage.FromPicture(svg.Picture, size); + using var image = SKImage.FromPicture(svg.Picture, size); var bitmap = SKBitmap.FromImage(image); return new Bitmap(bitmap, ImageFormat.Svg, 1); } else { - var image = SKImage.FromEncodedData(data); + using var image = SKImage.FromEncodedData(data); var codec = SKCodec.Create(image.EncodedData); if (!SK2GX.TryGetValue(codec.EncodedFormat, out var format)) @@ -404,7 +404,7 @@ public void Save(Stream stream, ImageFormat format, int quality = 100) if (!GX2SK.TryGetValue(format, out var skFormat)) throw new NotSupportedException($"unsupported save {format} format"); - var image = SKImage.FromBitmap(m_bitmap); + using var image = SKImage.FromBitmap(m_bitmap); // TODO: Only Png, Jpeg and Webp allowed to encode, otherwise returns null // ref: https://learn.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/bitmaps/saving#exploring-the-image-formats From 891dc289126730832d53d24e585d7902c17a4bc2 Mon Sep 17 00:00:00 2001 From: dsalvia Date: Tue, 18 Jun 2024 10:09:30 -0300 Subject: [PATCH 33/38] simplify SKRect to SKRectI --- src/Common/Bitmap.cs | 2 +- src/Common/Image.cs | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Common/Bitmap.cs b/src/Common/Bitmap.cs index 41d4293..c31e75b 100644 --- a/src/Common/Bitmap.cs +++ b/src/Common/Bitmap.cs @@ -93,7 +93,7 @@ public Bitmap(Image original, Size size) public object Clone(Rectangle rect, PixelFormat format) { var bitmap = new Bitmap(rect.Width, rect.Height); - var portion = new SKRectI((int)rect.Left, (int)rect.Top, (int)rect.Right, (int)rect.Bottom); + var portion = SKRectI.Truncate(rect.m_rect); return m_bitmap.ExtractSubset(bitmap.m_bitmap, portion) ? bitmap : base.Clone(); } diff --git a/src/Common/Image.cs b/src/Common/Image.cs index 5bfc8b5..fca9d1b 100644 --- a/src/Common/Image.cs +++ b/src/Common/Image.cs @@ -241,9 +241,8 @@ public static Image FromStream(Stream stream) var svg = new SkiaSharp.Extended.Svg.SKSvg(); svg.Load(data.AsStream()); - var bounds = svg.Picture.CullRect; - var size = new SKSizeI((int)bounds.Width, (int)bounds.Height); - using var image = SKImage.FromPicture(svg.Picture, size); + var bounds = SKRectI.Truncate(svg.Picture.CullRect); + using var image = SKImage.FromPicture(svg.Picture, bounds.Size); var bitmap = SKBitmap.FromImage(image); return new Bitmap(bitmap, ImageFormat.Svg, 1); From c91fd4cdc8d26d9c0802bb08348e5c0931686ca3 Mon Sep 17 00:00:00 2001 From: dsalvia Date: Tue, 18 Jun 2024 13:33:52 -0300 Subject: [PATCH 34/38] Define GetResourceStream in Image class (instead of Bitmap class) --- src/Common/Bitmap.cs | 17 +---------------- src/Common/Image.cs | 10 ++++++++++ 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/src/Common/Bitmap.cs b/src/Common/Bitmap.cs index c31e75b..ec71022 100644 --- a/src/Common/Bitmap.cs +++ b/src/Common/Bitmap.cs @@ -70,7 +70,7 @@ public Bitmap(Image original) /// Initializes a new instance of the class with the specified , width and height /// public Bitmap(Image original, float width, float height) - : this(original.m_bitmap.Resize(new SKImageInfo((int)width, (int)height), SKFilterQuality.High)) { } + : this(original.m_bitmap.Resize(new SKImageInfo((int)width, (int)height), SKFilterQuality.High)) { } /// /// Initializes a new instance of the class with the specified and @@ -230,19 +230,4 @@ public void UnlockBits(object bitmapdata) => throw new NotImplementedException(); // TODO: implement BitmapData #endregion - - - #region Utilities - - private static Stream GetResourceStream(Type type, string resource) - { - if (type == null) - throw new ArgumentNullException(nameof(type)); - if (resource == null) - throw new ArgumentNullException(nameof(resource)); - return type.Module.Assembly.GetManifestResourceStream(type, resource) - ?? throw new ArgumentException("resource not found", nameof(resource)); - } - - #endregion } diff --git a/src/Common/Image.cs b/src/Common/Image.cs index fca9d1b..7833d05 100644 --- a/src/Common/Image.cs +++ b/src/Common/Image.cs @@ -455,6 +455,16 @@ public void SetPropertyItem(object propitem) #region Utilities + protected static Stream GetResourceStream(Type type, string resource) + { + if (type == null) + throw new ArgumentNullException(nameof(type)); + if (resource == null) + throw new ArgumentNullException(nameof(resource)); + return type.Module.Assembly.GetManifestResourceStream(type, resource) + ?? throw new ArgumentException("resource not found", nameof(resource)); + } + // Indexing for frame-based images (e.g. gif) internal int m_index = 0; From bb90c291a9b90142b2579c4922c562c9dc7fa8d2 Mon Sep 17 00:00:00 2001 From: dsalvia Date: Tue, 18 Jun 2024 13:55:38 -0300 Subject: [PATCH 35/38] fix Bitmap(Type,string) description --- src/Common/Bitmap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Common/Bitmap.cs b/src/Common/Bitmap.cs index ec71022..8cc1973 100644 --- a/src/Common/Bitmap.cs +++ b/src/Common/Bitmap.cs @@ -24,7 +24,7 @@ public Bitmap(Stream stream, bool useIcm = true) : this(FromStream(stream)) { } /// - /// Initializes a new instance of the class from a file stream + /// Initializes a new instance of the from a specified resource /// public Bitmap(Type type, string resource) : this(GetResourceStream(type, resource)) { } From 870125ed4521ea2387cc620802426cd68887010e Mon Sep 17 00:00:00 2001 From: dsalvia Date: Fri, 21 Jun 2024 10:45:58 -0300 Subject: [PATCH 36/38] define Svg class, remove SKBitmap references from Image class and use them on Bitmap class --- src/Common/Bitmap.cs | 167 +++++++++++++++++++++++++++++++++++-- src/Common/Icon.cs | 6 +- src/Common/Image.cs | 160 +++++++----------------------------- src/Common/Svg.cs | 192 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 387 insertions(+), 138 deletions(-) create mode 100644 src/Common/Svg.cs diff --git a/src/Common/Bitmap.cs b/src/Common/Bitmap.cs index 8cc1973..fbbd463 100644 --- a/src/Common/Bitmap.cs +++ b/src/Common/Bitmap.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Linq; using GeneXus.Drawing.Imaging; using SkiaSharp; @@ -8,8 +9,21 @@ namespace GeneXus.Drawing; [Serializable] public sealed class Bitmap : Image, IDisposable, ICloneable { - internal Bitmap(SKBitmap bitmap, ImageFormat format = ImageFormat.Png, int frames = 1) - : base(bitmap, format, frames) { } + internal readonly SKBitmap m_bitmap; + + internal Bitmap(SKBitmap bitmap, ImageFormat format, int frames = 1) : base(format, frames, bitmap.Info.Size) + { + m_bitmap = bitmap ?? throw new ArgumentNullException(nameof(bitmap)); + + Width = m_bitmap.Width; + Height = m_bitmap.Height; + + Flags = GetFlags(m_bitmap); + PixelFormat = GetPixelFormat(m_bitmap.ColorType, m_bitmap.AlphaType, m_bitmap.BytesPerPixel, m_bitmap.Pixels); + } + + private Bitmap(Bitmap other) + : this(other.m_bitmap, other.m_format, other.m_frames) { } /// /// Initializes a new instance of the class from a filename @@ -33,7 +47,7 @@ public Bitmap(Type type, string resource) /// Initializes a new instance of the class with the specified width and height /// public Bitmap(float width, float height) - : this(new SKBitmap((int)width, (int)height)) { } + : this(new SKBitmap((int)width, (int)height), ImageFormat.Png, 1) { } /// /// Initializes a new instance of the class with the specified size @@ -70,7 +84,7 @@ public Bitmap(Image original) /// Initializes a new instance of the class with the specified , width and height /// public Bitmap(Image original, float width, float height) - : this(original.m_bitmap.Resize(new SKImageInfo((int)width, (int)height), SKFilterQuality.High)) { } + : this(Resize(original, width, height)) { } /// /// Initializes a new instance of the class with the specified and @@ -84,8 +98,23 @@ public Bitmap(Image original, Size size) public override string ToString() => $"{GetType().Name}: {Width}x{Height}"; + #region IDisposable + + /// + /// Cleans up resources for this . + /// + protected override void Dispose(bool disposing) => m_bitmap.Dispose(); + + #endregion + + #region IClonable + /// + /// Creates an exact copy of this . + /// + public override object Clone() => new Bitmap(m_bitmap.Copy(), m_format, m_frames); + /// /// Creates a copy of the section of this defined /// by structure and with a specified PixelFormat enumeration. @@ -94,7 +123,7 @@ public object Clone(Rectangle rect, PixelFormat format) { var bitmap = new Bitmap(rect.Width, rect.Height); var portion = SKRectI.Truncate(rect.m_rect); - return m_bitmap.ExtractSubset(bitmap.m_bitmap, portion) ? bitmap : base.Clone(); + return m_bitmap.ExtractSubset(bitmap.m_bitmap, portion) ? bitmap : Clone(); } #endregion @@ -204,6 +233,21 @@ public void MakeTransparent(Color transparentColor) SetPixel(x, y, Color.Transparent); } + /// + protected override void RotateFlip(int degrees, float scaleX, float scaleY) + { + float centerX = Width / 2f; + float centerY = Height / 2f; + + using var surface = SKSurface.Create(m_bitmap.Info); + surface.Canvas.RotateDegrees(degrees, centerX, centerY); + surface.Canvas.Scale(scaleX, scaleY, centerX, centerY); + surface.Canvas.DrawBitmap(m_bitmap, 0, 0); + + var bitmap = SKBitmap.FromImage(surface.Snapshot()); + m_bitmap.SetPixels(bitmap.GetPixels()); + } + /// /// Sets the color of the specified pixel in this . /// @@ -230,4 +274,117 @@ public void UnlockBits(object bitmapdata) => throw new NotImplementedException(); // TODO: implement BitmapData #endregion + + + #region Utilities + + internal override SKImage m_image => SKImage.FromBitmap(m_bitmap); + + private static Bitmap Resize(Image original, float width, float height) + { + if (original.m_image is not SKImage image) + throw new ArgumentException($"not image.", nameof(original)); + var info = new SKImageInfo((int)width, (int)height, image.ColorType, image.AlphaType, image.ColorSpace); + var bitmap = SKBitmap.FromImage(image).Resize(info, SKFilterQuality.High); + return new Bitmap(bitmap, original.m_format, original.m_frames); + } + + private int GetFlags(SKBitmap bitmap) + { + var flags = ImageFlags.None; + + if (bitmap.IsImmutable) + flags |= ImageFlags.ReadOnly; + + if (bitmap.Pixels.Any(pixel => pixel.Alpha < 255)) + flags |= ImageFlags.HasAlpha; + + if (bitmap.Pixels.Any(pixel => pixel.Alpha > 0 && pixel.Alpha < 255)) + flags |= ImageFlags.HasTranslucent; + + if (bitmap.Width > 0 && m_bitmap.Height > 0) + flags |= ImageFlags.HasRealPixelSize; + + switch (bitmap.ColorType) + { + case SKColorType.Rgb565: + case SKColorType.Rgb888x: + case SKColorType.Rgba8888: + case SKColorType.Bgra8888: + flags |= ImageFlags.ColorSpaceRgb; + break; + + case SKColorType.Gray8: + flags |= ImageFlags.ColorSpaceGray; + break; + } + + /* + * TODO: Missing flags + * - ImageFlags.Caching + * - ImageFlags.ColorSpaceCmyk + * - ImageFlags.ColorSpaceYcbcr + * - ImageFlags.ColorSpaceYcck + * - ImageFlags.HasRealDpi + * - ImageFlags.PartiallyScalable + * - ImageFlags.Scalable + */ + + return (int)flags; + } + + private static PixelFormat GetPixelFormat(SKColorType colorType, SKAlphaType alphaType, int bytesPerPixel, SKColor[] pixels) + { + if (alphaType == SKAlphaType.Opaque && pixels.All(pixel => pixel.Red == pixel.Green && pixel.Green == pixel.Blue)) + return PixelFormat.Format8bppIndexed; // NOTE: to behave as System.Drawing but SkiaSharp seems to treat it differently + + switch (colorType) + { + case SKColorType.Rgb565 when alphaType == SKAlphaType.Opaque: + return bytesPerPixel == 2 ? PixelFormat.Format16bppRgb565 + : PixelFormat.Undefined; + + case SKColorType.Rgb888x when alphaType == SKAlphaType.Unpremul: + return bytesPerPixel == 3 ? PixelFormat.Format24bppRgb + : bytesPerPixel == 4 ? PixelFormat.Format32bppRgb + : bytesPerPixel == 6 ? PixelFormat.Format48bppRgb + : PixelFormat.Undefined; + + case SKColorType.Rgba8888 when alphaType == SKAlphaType.Opaque: + case SKColorType.Bgra8888 when alphaType == SKAlphaType.Opaque: + return bytesPerPixel == 4 ? PixelFormat.Format24bppRgb + : bytesPerPixel == 6 ? PixelFormat.Format32bppRgb + : bytesPerPixel == 8 ? PixelFormat.Format48bppRgb + : PixelFormat.Undefined; + + case SKColorType.Rgba8888 when alphaType == SKAlphaType.Unpremul: + case SKColorType.Bgra8888 when alphaType == SKAlphaType.Unpremul: + return bytesPerPixel == 4 ? PixelFormat.Format32bppArgb + : bytesPerPixel == 8 ? PixelFormat.Format64bppArgb + : PixelFormat.Undefined; + + case SKColorType.Rgba8888 when alphaType == SKAlphaType.Premul: + case SKColorType.Bgra8888 when alphaType == SKAlphaType.Premul: + return bytesPerPixel == 4 ? PixelFormat.Format32bppPArgb + : bytesPerPixel == 8 ? PixelFormat.Format64bppPArgb + : PixelFormat.Undefined; + + case SKColorType.Gray8: + return bytesPerPixel == 2 ? PixelFormat.Format16bppGrayScale + : PixelFormat.Undefined; + + default: + /* + * TODO: missing value mappings + * - Format1bppIndexed + * - Format4bppIndexed + * - Format8bppIndexed + * - Format16bppRgb555 + * - Format16bppArgb1555 + */ + return PixelFormat.Undefined; + } + } + + #endregion } diff --git a/src/Common/Icon.cs b/src/Common/Icon.cs index b817722..78b371f 100644 --- a/src/Common/Icon.cs +++ b/src/Common/Icon.cs @@ -173,7 +173,7 @@ public static Icon FromHandle(IntPtr handle) var info = new SKImageInfo(100, 100); var skBitmap = new SKBitmap(); skBitmap.InstallPixels(info, handle, info.RowBytes); - var bitmap = new Bitmap(skBitmap); + var bitmap = new Bitmap(skBitmap, ImageFormat.Ico); return new Icon(bitmap); } @@ -219,13 +219,13 @@ public static Icon ExtractIcon(string filePath, int id, int size) /// Saves this to the specified output . /// public void Save(Stream stream) - => new Bitmap(Resized).Save(stream, ImageFormat.Png, 100); + => ToBitmap().Save(stream, ImageFormat.Png, 100); /// /// Converts this to a . /// public Bitmap ToBitmap() - => new(Resized); + => new(Resized, ImageFormat.Png); #endregion diff --git a/src/Common/Image.cs b/src/Common/Image.cs index 7833d05..455d816 100644 --- a/src/Common/Image.cs +++ b/src/Common/Image.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using System.Text; +using System.Xml; using GeneXus.Drawing.Imaging; using SkiaSharp; @@ -11,21 +12,20 @@ namespace GeneXus.Drawing; [Serializable] public abstract class Image : IDisposable, ICloneable { - internal readonly SKBitmap m_bitmap; internal readonly ImageFormat m_format; internal readonly int m_frames; private ColorPalette m_palette; - internal Image(SKBitmap bitmap, ImageFormat format, int frames) + internal Image(ImageFormat format, int frames, SKSize size) { - m_bitmap = bitmap; m_frames = frames; m_format = format; m_palette = new ColorPalette(); - using var surface = SKSurface.Create(new SKImageInfo(m_bitmap.Width, m_bitmap.Height)); + var info = new SKImageInfo((int)size.Width, (int)size.Height); + using var surface = SKSurface.Create(info); HorizontalResolution = (int)(100f * surface.Canvas.DeviceClipBounds.Width / surface.Canvas.LocalClipBounds.Width); VerticalResolution = (int)(100f * surface.Canvas.DeviceClipBounds.Height / surface.Canvas.LocalClipBounds.Height); } @@ -52,7 +52,7 @@ public void Dispose() Dispose(true); } - protected virtual void Dispose(bool disposing) => m_bitmap.Dispose(); + protected abstract void Dispose(bool disposing); #endregion @@ -62,7 +62,7 @@ public void Dispose() /// /// Creates an exact copy of this . /// - public virtual object Clone() => new Bitmap(m_bitmap.Copy()); + public abstract object Clone(); #endregion @@ -72,7 +72,7 @@ public void Dispose() /// /// Creates a with the coordinates of the specified . /// - public static explicit operator SKImage(Image image) => SKImage.FromBitmap(image.m_bitmap); + public static explicit operator SKImage(Image image) => image.m_image; #endregion @@ -94,52 +94,7 @@ public void Dispose() /// Gets attribute flags for the pixel data of this defined by the /// bitwise combination of . /// - public int Flags - { - get - { - var flags = ImageFlags.None; - - if (m_bitmap.IsImmutable) - flags |= ImageFlags.ReadOnly; - - if (m_bitmap.Pixels.Any(pixel => pixel.Alpha < 255)) - flags |= ImageFlags.HasAlpha; - - if (m_bitmap.Pixels.Any(pixel => pixel.Alpha > 0 && pixel.Alpha < 255)) - flags |= ImageFlags.HasTranslucent; - - if (m_bitmap.Width > 0 && m_bitmap.Height > 0) - flags |= ImageFlags.HasRealPixelSize; - - switch (m_bitmap.ColorType) - { - case SKColorType.Rgb565: - case SKColorType.Rgb888x: - case SKColorType.Rgba8888: - case SKColorType.Bgra8888: - flags |= ImageFlags.ColorSpaceRgb; - break; - - case SKColorType.Gray8: - flags |= ImageFlags.ColorSpaceGray; - break; - } - - /* - * TODO: Missing flags - * - ImageFlags.Caching - * - ImageFlags.ColorSpaceCmyk - * - ImageFlags.ColorSpaceYcbcr - * - ImageFlags.ColorSpaceYcck - * - ImageFlags.HasRealDpi - * - ImageFlags.PartiallyScalable - * - ImageFlags.Scalable - */ - - return (int)flags; - } - } + public int Flags { get; protected set; } /// /// Gets an array of GUIDs that represent the dimensions of frames within this . @@ -149,7 +104,7 @@ public int Flags /// /// Gets the height of this . /// - public int Height => m_bitmap.Height; + public int Height { get; protected set; } /// /// Gets the horizontal resolution, in pixels-per-inch, of this . @@ -173,7 +128,7 @@ public ColorPalette Palette /// /// Gets the for this . /// - public PixelFormat PixelFormat => ToPixelFormat(m_bitmap.ColorType, m_bitmap.AlphaType, m_bitmap.BytesPerPixel, m_bitmap.Pixels); + public PixelFormat PixelFormat { get; protected set; } /// /// Gets IDs of the property items stored in this . @@ -208,7 +163,7 @@ public ColorPalette Palette /// /// Gets the width of this . /// - public int Width => m_bitmap.Width; + public int Width { get; protected set; } #endregion @@ -240,12 +195,11 @@ public static Image FromStream(Stream stream) { var svg = new SkiaSharp.Extended.Svg.SKSvg(); svg.Load(data.AsStream()); - - var bounds = SKRectI.Truncate(svg.Picture.CullRect); - using var image = SKImage.FromPicture(svg.Picture, bounds.Size); - var bitmap = SKBitmap.FromImage(image); - return new Bitmap(bitmap, ImageFormat.Svg, 1); + var doc = new XmlDocument(); + doc.Load(data.AsStream()); + + return new Svg(svg, doc); } else { @@ -305,8 +259,9 @@ public object GetPropertyItem(int propid) public Image GetThumbnailImage(int thumbWidth, int thumbHeight, GetThumbnailImageAbort callback, IntPtr callbackData) { // NOTE: callback and callbackData parameters are ignored according to the documentation - var info = new SKImageInfo(thumbWidth, thumbHeight, m_bitmap.ColorType, m_bitmap.AlphaType, m_bitmap.ColorSpace); - var thumb = m_bitmap.Resize(info, SKFilterQuality.High); + var bitmap = SKBitmap.FromImage(m_image); + var info = new SKImageInfo(thumbWidth, thumbHeight, bitmap.ColorType, bitmap.AlphaType, bitmap.ColorSpace); + var thumb = bitmap.Resize(info, SKFilterQuality.High); return thumb == null ? throw new Exception("could not resize.") : new Bitmap(thumb, m_format, m_frames); @@ -353,20 +308,18 @@ public void RotateFlip(RotateFlipType rotateFlipType) RotateFlipType.Rotate90FlipXY => (90, -1, -1), _ => throw new ArgumentException($"unrecognized value {rotateFlipType}", nameof(rotateFlipType)) }; - - using var surface = SKSurface.Create(m_bitmap.Info); - surface.Canvas.RotateDegrees(degrees, Width / 2f, Height / 2f); - surface.Canvas.Scale(scaleX, scaleY, Width / 2f, Height / 2f); - surface.Canvas.DrawBitmap(m_bitmap, 0, 0); - - var rotated = SKBitmap.FromImage(surface.Snapshot()); - m_bitmap.SetPixels(rotated.GetPixels()); + RotateFlip(degrees, scaleX, scaleY); } + /// + /// Rotates and scales the . + /// + protected abstract void RotateFlip(int degrees, float scaleX, float scaleY); + /// /// Saves this to the specified file. /// - public void Save(string filename) => Save(filename, RawFormat); + public virtual void Save(string filename) => Save(filename, RawFormat); /// /// Saves this to the specified file in the specified format. @@ -387,7 +340,7 @@ public void Save(string filename, ImageFormat format, int quality = 100) /// /// Saves this to the specified file with the specified encoder and parameters. /// - public void Save(string filename, object encoder, object encoderParams) // TODO: implement EncoderParameters + public void Save(string filename, object encoder, object encoderParams) // TODO: implement ImageCodecInfo & EncoderParameters { var file = new FileInfo(filename); var stream = file.OpenWrite(); @@ -400,14 +353,12 @@ public void Save(string filename, object encoder, object encoderParams) // TODO: /// public void Save(Stream stream, ImageFormat format, int quality = 100) { - if (!GX2SK.TryGetValue(format, out var skFormat)) + if (!GX2SK.TryGetValue(format, out var encoder)) throw new NotSupportedException($"unsupported save {format} format"); - using var image = SKImage.FromBitmap(m_bitmap); - // TODO: Only Png, Jpeg and Webp allowed to encode, otherwise returns null // ref: https://learn.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/bitmaps/saving#exploring-the-image-formats - var data = image.Encode(skFormat, quality) ?? throw new NotSupportedException($"unsupported encoding format {format}"); + var data = m_image.Encode(encoder, quality) ?? throw new NotSupportedException($"unsupported encoding format {format}"); data.SaveTo(stream); data.Dispose(); } @@ -455,6 +406,8 @@ public void SetPropertyItem(object propitem) #region Utilities + internal abstract SKImage m_image { get; } + protected static Stream GetResourceStream(Type type, string resource) { if (type == null) @@ -494,58 +447,5 @@ private static bool IsSvg(SKData data) return Encoding.UTF8.GetString(header) == " pixel.Red == pixel.Green && pixel.Green == pixel.Blue)) - return PixelFormat.Format8bppIndexed; // NOTE: to behave as System.Drawing but SkiaSharp seems to treat it differently - - switch (colorType) - { - case SKColorType.Rgb565 when alphaType == SKAlphaType.Opaque: - return bytesPerPixel == 2 ? PixelFormat.Format16bppRgb565 - : PixelFormat.Undefined; - - case SKColorType.Rgb888x when alphaType == SKAlphaType.Unpremul: - return bytesPerPixel == 3 ? PixelFormat.Format24bppRgb - : bytesPerPixel == 4 ? PixelFormat.Format32bppRgb - : bytesPerPixel == 6 ? PixelFormat.Format48bppRgb - : PixelFormat.Undefined; - - case SKColorType.Rgba8888 when alphaType == SKAlphaType.Opaque: - case SKColorType.Bgra8888 when alphaType == SKAlphaType.Opaque: - return bytesPerPixel == 4 ? PixelFormat.Format24bppRgb - : bytesPerPixel == 6 ? PixelFormat.Format32bppRgb - : bytesPerPixel == 8 ? PixelFormat.Format48bppRgb - : PixelFormat.Undefined; - - case SKColorType.Rgba8888 when alphaType == SKAlphaType.Unpremul: - case SKColorType.Bgra8888 when alphaType == SKAlphaType.Unpremul: - return bytesPerPixel == 4 ? PixelFormat.Format32bppArgb - : bytesPerPixel == 8 ? PixelFormat.Format64bppArgb - : PixelFormat.Undefined; - - case SKColorType.Rgba8888 when alphaType == SKAlphaType.Premul: - case SKColorType.Bgra8888 when alphaType == SKAlphaType.Premul: - return bytesPerPixel == 4 ? PixelFormat.Format32bppPArgb - : bytesPerPixel == 8 ? PixelFormat.Format64bppPArgb - : PixelFormat.Undefined; - - case SKColorType.Gray8: - return bytesPerPixel == 2 ? PixelFormat.Format16bppGrayScale - : PixelFormat.Undefined; - - default: - /* - * TODO: missing value mappings - * - Format1bppIndexed - * - Format4bppIndexed - * - Format8bppIndexed - * - Format16bppRgb555 - * - Format16bppArgb1555 - */ - return PixelFormat.Undefined; - } - } - #endregion } diff --git a/src/Common/Svg.cs b/src/Common/Svg.cs new file mode 100644 index 0000000..2895fe2 --- /dev/null +++ b/src/Common/Svg.cs @@ -0,0 +1,192 @@ +using System; +using System.IO; +using System.Text; +using System.Xml; +using GeneXus.Drawing.Imaging; +using SkiaSharp; + +namespace GeneXus.Drawing; + +public class Svg : Image +{ + internal SkiaSharp.Extended.Svg.SKSvg m_svg; + internal XmlDocument m_doc = new(); + + internal Svg(SkiaSharp.Extended.Svg.SKSvg svg, XmlDocument doc) : base(ImageFormat.Svg, 1, svg.ViewBox.Size) + { + m_svg = svg ?? throw new ArgumentNullException(nameof(svg)); + m_doc = doc ?? throw new ArgumentNullException(nameof(doc)); + + Width = (int)m_svg.Picture.CullRect.Width; + Height = (int)m_svg.Picture.CullRect.Height; + + Flags = (int)(ImageFlags.ReadOnly | ImageFlags.Scalable); + PixelFormat = PixelFormat.Undefined; + } + + private Svg(Svg other) + : this(other.m_svg, other.m_doc) { } + + /// + /// Initializes a new instance of the class from a filename + /// + public Svg(string filename) + : this(FromFile(filename)) { } + + /// + /// Initializes a new instance of the class from a file stream + /// + public Svg(Stream stream) + : this(FromStream(stream)) { } + + /// + /// Initializes a new instance of the class with the specified + /// + public Svg(Image original) + : this(original, original.Width, original.Height) { } + + /// + /// Initializes a new instance of the class with the specified , width and height + /// + public Svg(Image original, float width, float height) + : this(Resize(original, width, height)) { } + + /// + /// Initializes a new instance of the class with the specified and + /// + public Svg(Image original, Size size) + : this(original, size.Width, size.Height) { } + + /// + /// Creates a human-readable string that represents this . + /// + public override string ToString() => $"{GetType().Name}: {Width}x{Height}"; + + + #region IDisposable + + /// + /// Cleans up resources for this . + /// + protected override void Dispose(bool disposing) { } + + #endregion + + + #region IClonable + + /// + /// Creates an exact copy of this . + /// + public override object Clone() => new Svg(m_svg, m_doc); + + #endregion + + + #region Operators + + /// + /// Creates a with the coordinates of the specified . + /// + public static explicit operator SkiaSharp.Extended.Svg.SKSvg(Svg svg) => svg.m_svg; + + #endregion + + + #region Properties + + /// + /// Gets the XML string of this . + /// + public string Xml => m_doc.OuterXml; + + #endregion + + + #region Factory + + /// + /// Creates an from the specified data xml string. + /// + public static Svg FromXml(string xml) + { + using var reader = new XmlTextReader(new StringReader(xml)); + + var svg = new SkiaSharp.Extended.Svg.SKSvg(); + svg.Load(reader); + + var doc = new XmlDocument(); + doc.LoadXml(xml); + + return new Svg(svg, doc); + } + + #endregion + + + #region Methods + + /// + protected override void RotateFlip(int degrees, float scaleX, float scaleY) + { + var width = (int)Math.Abs(scaleX * Width); + var height = (int)Math.Abs(scaleX * Height); + + m_doc.DocumentElement.SetAttribute("width", $"{width}"); + m_doc.DocumentElement.SetAttribute("height", $"{height}"); + m_doc.DocumentElement.SetAttribute("viewBox", $"0 0 {width} {height}"); + + var group = m_doc.SelectSingleNode("//g"); + if (group == null) + { + group = m_doc.CreateElement("g", m_doc.DocumentElement.NamespaceURI); + foreach (var tag in SHAPE_TAGS) + { + var shapes = m_doc.GetElementsByTagName(tag); + for (int i = 0; i < shapes.Count; i++) + group.AppendChild(shapes[i]); + } + m_doc.DocumentElement.PrependChild(group); + } + + var centerX = Width / 2f; + var centerY = Height / 2f; + + var offsetX = scaleX < 0 ? Width : 0; + var offsetY = scaleY < 0 ? Height : 0; + + var transform = m_doc.CreateAttribute("transform"); + transform.Value = string.Join(" ", group.Attributes["transform"]?.Value ?? string.Empty, + $"translate({offsetX} {offsetY})", + $"scale({scaleX}, {scaleY})", + $"rotate({degrees} {centerX} {centerY})").Trim(); + group.Attributes.SetNamedItem(transform); + + using var reader = new XmlNodeReader(m_doc); + m_svg.Load(reader); + } + + /// + /// Saves this to the specified file. + /// + public override void Save(string filename) => m_doc.Save(filename); + + #endregion + + + #region Utilities + + private static readonly string[] SHAPE_TAGS = { "path", "rect", "circle", "ellipse", "line", "polyline", "polygon" }; + + internal override SKImage m_image + => SKImage.FromPicture(m_svg.Picture, SKRectI.Truncate(m_svg.Picture.CullRect).Size); + + private static Svg Resize(Image original, float width, float height) + { + var result = original.Clone() is Svg svg ? svg : throw new NotImplementedException("image to svg."); + result.RotateFlip(0, width / original.Width, height / original.Height); + return result; + } + + #endregion +} From 2a7e006d46e5c26a0052031c6e14c795156ad1f5 Mon Sep 17 00:00:00 2001 From: dsalvia Date: Fri, 21 Jun 2024 14:32:32 -0300 Subject: [PATCH 37/38] define SV as sealed --- src/Common/Svg.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Common/Svg.cs b/src/Common/Svg.cs index 2895fe2..69ab211 100644 --- a/src/Common/Svg.cs +++ b/src/Common/Svg.cs @@ -7,7 +7,8 @@ namespace GeneXus.Drawing; -public class Svg : Image +[Serializable] +public sealed class Svg : Image { internal SkiaSharp.Extended.Svg.SKSvg m_svg; internal XmlDocument m_doc = new(); From 1e36251b818c51a9e02b76627100dcd79b7f00f0 Mon Sep 17 00:00:00 2001 From: Fede Azzato Date: Mon, 24 Jun 2024 11:35:24 -0300 Subject: [PATCH 38/38] Rename m_Image to InnerImage for property naming consistency. --- src/Common/Bitmap.cs | 4 ++-- src/Common/Image.cs | 8 ++++---- src/Common/Svg.cs | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Common/Bitmap.cs b/src/Common/Bitmap.cs index fbbd463..756e8d7 100644 --- a/src/Common/Bitmap.cs +++ b/src/Common/Bitmap.cs @@ -278,11 +278,11 @@ public void UnlockBits(object bitmapdata) #region Utilities - internal override SKImage m_image => SKImage.FromBitmap(m_bitmap); + internal override SKImage InnerImage => SKImage.FromBitmap(m_bitmap); private static Bitmap Resize(Image original, float width, float height) { - if (original.m_image is not SKImage image) + if (original.InnerImage is not SKImage image) throw new ArgumentException($"not image.", nameof(original)); var info = new SKImageInfo((int)width, (int)height, image.ColorType, image.AlphaType, image.ColorSpace); var bitmap = SKBitmap.FromImage(image).Resize(info, SKFilterQuality.High); diff --git a/src/Common/Image.cs b/src/Common/Image.cs index 455d816..4f5bef2 100644 --- a/src/Common/Image.cs +++ b/src/Common/Image.cs @@ -72,7 +72,7 @@ public void Dispose() /// /// Creates a with the coordinates of the specified . /// - public static explicit operator SKImage(Image image) => image.m_image; + public static explicit operator SKImage(Image image) => image.InnerImage; #endregion @@ -259,7 +259,7 @@ public object GetPropertyItem(int propid) public Image GetThumbnailImage(int thumbWidth, int thumbHeight, GetThumbnailImageAbort callback, IntPtr callbackData) { // NOTE: callback and callbackData parameters are ignored according to the documentation - var bitmap = SKBitmap.FromImage(m_image); + var bitmap = SKBitmap.FromImage(InnerImage); var info = new SKImageInfo(thumbWidth, thumbHeight, bitmap.ColorType, bitmap.AlphaType, bitmap.ColorSpace); var thumb = bitmap.Resize(info, SKFilterQuality.High); return thumb == null @@ -358,7 +358,7 @@ public void Save(Stream stream, ImageFormat format, int quality = 100) // TODO: Only Png, Jpeg and Webp allowed to encode, otherwise returns null // ref: https://learn.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/bitmaps/saving#exploring-the-image-formats - var data = m_image.Encode(encoder, quality) ?? throw new NotSupportedException($"unsupported encoding format {format}"); + var data = InnerImage.Encode(encoder, quality) ?? throw new NotSupportedException($"unsupported encoding format {format}"); data.SaveTo(stream); data.Dispose(); } @@ -406,7 +406,7 @@ public void SetPropertyItem(object propitem) #region Utilities - internal abstract SKImage m_image { get; } + internal abstract SKImage InnerImage { get; } protected static Stream GetResourceStream(Type type, string resource) { diff --git a/src/Common/Svg.cs b/src/Common/Svg.cs index 69ab211..e8c7150 100644 --- a/src/Common/Svg.cs +++ b/src/Common/Svg.cs @@ -179,7 +179,7 @@ protected override void RotateFlip(int degrees, float scaleX, float scaleY) private static readonly string[] SHAPE_TAGS = { "path", "rect", "circle", "ellipse", "line", "polyline", "polygon" }; - internal override SKImage m_image + internal override SKImage InnerImage => SKImage.FromPicture(m_svg.Picture, SKRectI.Truncate(m_svg.Picture.CullRect).Size); private static Svg Resize(Image original, float width, float height)