diff --git a/src/Common/Bitmap.cs b/src/Common/Bitmap.cs
index 7505e7d..756e8d7 100644
--- a/src/Common/Bitmap.cs
+++ b/src/Common/Bitmap.cs
@@ -1,37 +1,72 @@
using System;
using System.IO;
+using System.Linq;
using GeneXus.Drawing.Imaging;
using SkiaSharp;
namespace GeneXus.Drawing;
[Serializable]
-public class Bitmap : IDisposable, ICloneable
+public sealed class Bitmap : Image, IDisposable, ICloneable
{
internal readonly SKBitmap m_bitmap;
- internal Bitmap(SKBitmap skBitmap)
+ internal Bitmap(SKBitmap bitmap, ImageFormat format, int frames = 1) : base(format, frames, bitmap.Info.Size)
{
- m_bitmap = skBitmap;
+ 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
///
- 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 from a specified resource
+ ///
+ public Bitmap(Type type, string resource)
+ : this(GetResourceStream(type, 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
+ /// 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, 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, PixelFormat format, IntPtr scan0)
+ : this(width, height) => throw new NotImplementedException();
///
/// Initializes a new instance of the class with the specified
@@ -43,31 +78,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(Resize(original, width, height)) { }
///
/// Initializes a new instance of the class with the specified and
@@ -75,11 +92,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();
-
///
/// Creates a human-readable string that represents this .
///
@@ -91,7 +103,7 @@ public Bitmap(Image original, Size size)
///
/// Cleans up resources for this .
///
- public void Dispose() => m_bitmap.Dispose();
+ protected override void Dispose(bool disposing) => m_bitmap.Dispose();
#endregion
@@ -101,7 +113,18 @@ public Bitmap(Image original, Size size)
///
/// Creates an exact copy of this .
///
- public object Clone() => new Bitmap(m_bitmap.Copy());
+ 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.
+ ///
+ 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 : Clone();
+ }
#endregion
@@ -109,7 +132,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 +142,56 @@ public Bitmap(Image original, Size size)
#region Properties
///
- /// Gets the width of this .
+ /// Gets the bytes-per-pixel value of this .
+ ///
+ public int BytesPerPixel => m_bitmap.BytesPerPixel;
+
+ ///
+ /// Gets the handle of this .
///
- public int Width => m_bitmap.Width;
+ public IntPtr Handle => m_bitmap.Handle;
+
+ #endregion
+
+
+ #region Factory
///
- /// Gets the height of this .
+ /// Creates a from a Windows handle to an icon.
///
- public int Height => m_bitmap.Height;
+ public static Bitmap FromHicon(IntPtr hicon)
+ => throw new NotSupportedException("windows specific");
///
- /// Gets the bytes-per-pixel value of this .
+ /// Creates a from the specified Windows resource.
///
- public int BytesPerPixel => m_bitmap.BytesPerPixel;
+ 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 +201,53 @@ 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, 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, PixelFormat format, object bitmapData)
+ => throw new NotImplementedException(); // TODO: implement BitmapData
+
+ ///
+ /// 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.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 .
///
@@ -156,20 +257,133 @@ public void SetPixel(int x, int y, Color color)
m_bitmap.SetPixel(x, y, c);
}
+ ///
+ /// Sets the resolution for this .
+ ///
+ public void SetResolution(float xDpi, float yDpi)
+ {
+ 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
+ }
+
+ ///
+ /// Unlocks this from system memory.
+ ///
+ public void UnlockBits(object bitmapdata)
+ => throw new NotImplementedException(); // TODO: implement BitmapData
+
#endregion
#region Utilities
- ///
- /// Saves this into a stream with a specified format.
- ///
- internal void Save(Stream stream, SKEncodedImageFormat format, int quality = 100)
+ internal override SKImage InnerImage => SKImage.FromBitmap(m_bitmap);
+
+ private static Bitmap Resize(Image original, float width, float height)
+ {
+ 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);
+ 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)
{
- 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 (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/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/Icon.cs b/src/Common/Icon.cs
index bbc38d7..78b371f 100644
--- a/src/Common/Icon.cs
+++ b/src/Common/Icon.cs
@@ -2,16 +2,17 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using GeneXus.Drawing.Imaging;
using SkiaSharp;
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;
- internal int m_index { get; private set; } = 0;
+ internal int EntryIndex { get; private set; }
private Icon(Bitmap bitmap, List entries, float width, float height)
{
@@ -30,7 +31,7 @@ private Icon(Bitmap bitmap, List entries, float width, float height)
if (diff >= pivot)
continue;
pivot = diff;
- m_index = i;
+ EntryIndex = i;
}
}
@@ -38,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.
@@ -62,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.
@@ -80,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
@@ -99,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 .
@@ -112,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
@@ -134,34 +141,91 @@ 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 .
///
- public int Height => m_entries[m_index].Height;
+ public int Height => m_entries[EntryIndex].Height;
///
/// Gets the size of this .
///
public Size Size => new(Width, Height);
+ ///
+ /// Gets the width of this .
+ ///
+ public int Width => m_entries[EntryIndex].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, ImageFormat.Ico);
+ 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); // 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 (icon.Width == size && icon.Height == size)
+ id = int.MaxValue; // set to undefined
+
+ if (id >= 0 && id < icon.m_entries.Count)
+ icon.EntryIndex = id; // set defined index
+
+ 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)
+ => ToBitmap().Save(stream, ImageFormat.Png, 100);
+
+ ///
+ /// Converts this to a .
+ ///
+ public Bitmap ToBitmap()
+ => new(Resized, ImageFormat.Png);
#endregion
@@ -214,11 +278,11 @@ private static List ReadIco(Stream stream)
return entries;
}
- private SKBitmap m_Resized
+ private SKBitmap 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;
diff --git a/src/Common/Image.cs b/src/Common/Image.cs
index 6b93915..4f5bef2 100644
--- a/src/Common/Image.cs
+++ b/src/Common/Image.cs
@@ -3,29 +3,37 @@
using System.IO;
using System.Linq;
using System.Text;
+using System.Xml;
using GeneXus.Drawing.Imaging;
using SkiaSharp;
namespace GeneXus.Drawing;
[Serializable]
-public class Image : IDisposable, ICloneable
+public abstract class Image : IDisposable, ICloneable
{
- internal readonly SKImage m_image;
- internal readonly SKCodec m_codec;
internal readonly ImageFormat m_format;
+ internal readonly int m_frames;
- internal Image(SKImage skImage, SKData skData, ImageFormat format)
+ private ColorPalette m_palette;
+
+ internal Image(ImageFormat format, int frames, SKSize size)
{
- m_image = skImage;
- m_codec = SKCodec.Create(skData);
+ m_frames = frames;
m_format = format;
+
+ m_palette = new ColorPalette();
+
+ 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);
}
///
/// Cleans up resources for this .
///
- ~Image() => Dispose();
+ ~Image() => Dispose(false);
///
/// Creates a human-readable string that represents this .
@@ -40,10 +48,12 @@ internal Image(SKImage skImage, SKData skData, ImageFormat format)
///
public void Dispose()
{
- m_image.Dispose();
- m_codec?.Dispose();
+ GC.SuppressFinalize(this);
+ Dispose(true);
}
+ protected abstract void Dispose(bool disposing);
+
#endregion
@@ -52,13 +62,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 abstract object Clone();
#endregion
@@ -68,7 +72,18 @@ 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) => image.InnerImage;
+
+ #endregion
+
+
+ #region Delegates
+
+ ///
+ /// Provides a callback method for determining when the method
+ /// should prematurely cancel execution.
+ ///
+ public delegate bool GetThumbnailImageAbort();
#endregion
@@ -76,14 +91,54 @@ public virtual object Clone()
#region Properties
///
- /// Gets the width of this .
+ /// Gets attribute flags for the pixel data of this defined by the
+ /// bitwise combination of .
///
- public int Width => m_image.Width;
+ public int Flags { get; protected set; }
+
+ ///
+ /// 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 { get; protected set; }
+
+ ///
+ /// 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 ColorPalette Palette
+ {
+ get => m_palette;
+ set => m_palette = value.Entries.Length > 0 ? value : throw new ArgumentException("parameter is not valid.");
+ }
+
+ ///
+ /// Gets the width and height of this .
+ ///
+ public Size PhysicalDimension => throw new NotImplementedException();
+
+ ///
+ /// Gets the for this .
+ ///
+ public PixelFormat PixelFormat { get; protected set; }
+
+ ///
+ /// 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 +150,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 { get; protected set; }
+
#endregion
@@ -109,6 +179,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.
///
@@ -120,21 +196,21 @@ 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 doc = new XmlDocument();
+ doc.Load(data.AsStream());
- var image = SKImage.FromPicture(svg.Picture, size);
- return new Image(image, data, ImageFormat.Svg);
+ return new Svg(svg, doc);
}
else
{
- var image = SKImage.FromEncodedData(data);
+ using 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 Bitmap(bitmap, format, Math.Max(codec.FrameCount, 1));
}
}
@@ -143,10 +219,107 @@ public static Image FromStream(Stream stream)
#region Methods
+ ///
+ /// Gets a bounding rectangle in the specified units for this .
+ ///
+ 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 .
+ ///
+ 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(PixelFormat pixfmt)
+ => ((int)pixfmt >> 8) & 0xFF;
+
+ ///
+ /// 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, GetThumbnailImageAbort callback, IntPtr callbackData)
+ {
+ // NOTE: callback and callbackData parameters are ignored according to the documentation
+ 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
+ ? throw new Exception("could not resize.")
+ : new Bitmap(thumb, m_format, m_frames);
+ }
+
+ ///
+ /// Returns a value indicating whether the pixel format contains alpha information.
+ ///
+ public static bool IsAlphaPixelFormat(PixelFormat pixfmt)
+ => (pixfmt & PixelFormat.Alpha) != 0;
+
+ ///
+ /// Returns a value indicating whether the pixel format is canonical.
+ ///
+ public static bool IsCanonicalPixelFormat(PixelFormat pixfmt)
+ => (pixfmt & PixelFormat.Canonical) != 0;
+
+ ///
+ /// Returns a value indicating whether the pixel format is extended.
+ ///
+ public static bool IsExtendedPixelFormat(PixelFormat pixfmt)
+ => (pixfmt & PixelFormat.Extended) != 0;
+
+ ///
+ /// 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(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))
+ };
+ 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.
@@ -164,33 +337,49 @@ 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 ImageCodecInfo & 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 encoder))
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);
-
// 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 = InnerImage.Encode(encoder, 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 int GetFrameCount() => m_frames;
+ 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 void SaveAdd(Image image, object encoderParams) // TODO: implement EncoderParameters
+ => throw new NotImplementedException();
///
/// Selects the frame specified by the given dimension and index.
@@ -206,31 +395,50 @@ 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
#region Utilities
+ internal abstract SKImage InnerImage { get; }
+
+ 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;
- 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)
{
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
+}
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
+}
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
+}
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
+}
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
+}
diff --git a/src/Common/Svg.cs b/src/Common/Svg.cs
new file mode 100644
index 0000000..e8c7150
--- /dev/null
+++ b/src/Common/Svg.cs
@@ -0,0 +1,193 @@
+using System;
+using System.IO;
+using System.Text;
+using System.Xml;
+using GeneXus.Drawing.Imaging;
+using SkiaSharp;
+
+namespace GeneXus.Drawing;
+
+[Serializable]
+public sealed 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 InnerImage
+ => 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
+}
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);
+ });
+ }
}
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));
+ });
+ }
}
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));
+ });
+ }
}