diff --git a/.gitignore b/.gitignore
index cd0f87e..4859bcf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,5 @@ bin/
obj/
/_packages
GeneXus.Drawing-DotNet.sln.DotSettings.user
+/Test/Common/res/images/.out
+.DS_Store
diff --git a/README.md b/README.md
index b3d99bf..7d43fe2 100644
--- a/README.md
+++ b/README.md
@@ -38,6 +38,8 @@ This repository is organized as follows:
Root
├─ src : source code
│ ├─ Common
+│ │ ├─ Drawing2D : elements for GeneXus.Drawing.Drawing2D
+│ │ │ └─ ...
│ │ ├─ Imaging : elements for GeneXus.Drawing.Imaging
│ │ │ └─ ...
│ │ ├─ Interop : elements for GeneXus.Drawing.Interop
@@ -46,10 +48,12 @@ Root
│ │ │ └─ ...
│ │ └─ ... : elements for GeneXus.Drawing
│ │
-│ └─ PackageREADME.md
+│ └─ PackageREADME.md : package readme file
│
├─ test : unit tests
│ ├─ Common
+│ │ ├─ Drawing2D : unit-tests for GeneXus.Drawing.Drawing2D
+│ │ │ └─ ...
│ │ ├─ Text : unit-tests for GeneXus.Drawing.Text
│ │ │ └─ ...
│ │ └─ ... : unit-tests for GeneXus.Drawing
@@ -75,28 +79,71 @@ This section describes each module (namespace) specifiying which elements are pa
### GeneXus.Drawing.Common
Basic graphics funcionalities based on `System.Drawing`.
-| Name | Type | Description
-|----------------------|----------|--------------
-| `Bitmap` | Class | Represents an image defined by pixels.
-| `Color` | Class | Defines colors used for drawing.
-| `Font` | Class | Defines a format for text, including font family, size, and style.
-| `Icon` | Class | Represents an icon image.
-| `Image` | Class | Represents an image in a specific format.
-| `Svg` (1) | Class | Represents Scalable Vector Graphics.
-| `Point` | Struct | Defines an x and y coordinate in a 2D plane.
-| `PointF` | Struct | Defines a floating-point x and y coordinate in a 2D plane.
-| `Rectangle` | Struct | Defines an x, y, width, and height of a rectangle.
-| `RectangleF` | Struct | Defines a floating-point x, y, width, and height of a rectangle.
-| `Size` | Struct | Defines the width and height of a rectangular area.
-| `SizeF` | Struct | Defines the width and height of a rectangular area with floating-point values.
-| `FontSlant` | Enum | Specifies the slant of a font.
-| `FontStyle` | Enum | Specifies the style of a font.
-| `GraphicsUnit` | Enum | Specifies the unit of measure for drawing operations.
-| `KnownColor` | Enum | Defines predefined colors.
-| `RotateFlipType` | Enum | Specifies how an image is rotated or flipped.
+| Name | Type | Description
+|-------------------------|----------|--------------
+| `Bitmap` | Class | Represents an image defined by pixels.
+| `Brush` | Class | Abstract class for brushes used to fill graphics shapes.
+| `Color` | Class | Defines colors used for drawing.
+| `ColorConverter` | Class | Converts colors from one data type to another
+| `ColorTranslator` | Class | Translates colors to and from HTML, OLE and Win32 representations.
+| `Font` | Class | Defines a format for text, including font family, size, and style.
+| `Graphics` | Class | Provides methods for drawing on a drawing surface.
+| `Icon` | Class | Represents an icon image.
+| `Image` | Class | Represents an image in a specific format.
+| `Pen` | Class | Defines an object used to draw lines and curves.
+| `Region` | Class | Defines the area of a drawing surface.
+| `SolidBrush` | Class | Defines a brush of a single color.
+| `StringFormat` | Class | Defines text layout information, display manipulation and font features.
+| `Svg` (1) | Class | Represents Scalable Vector Graphics.
+| `TextureBrush` | Class | Defines a brush that uses an image to fill shapes.
+| `Point` | Struct | Defines an x and y coordinate in a 2D plane.
+| `PointF` | Struct | Defines a floating-point x and y coordinate in a 2D plane.
+| `Rectangle` | Struct | Defines an x, y, width, and height of a rectangle.
+| `RectangleF` | Struct | Defines a floating-point x, y, width, and height of a rectangle.
+| `Size` | Struct | Defines the width and height of a rectangular area.
+| `SizeF` | Struct | Defines the width and height of a rectangular area with floating-point values.
+| `CopyPixelOperation` | Enum | Specifies the type of pixel copying operation.
+| `FontSlant` | Enum | Specifies the slant of a font.
+| `FontStyle` | Enum | Specifies the style of a font.
+| `GraphicsUnit` | Enum | Specifies the unit of measure for drawing operations.
+| `KnownColor` | Enum | Defines predefined colors.
+| `RotateFlipType` | Enum | Specifies how an image is rotated or flipped.
+| `StringAlignment` | Enum | Specifies the alignment of text within a string.
+| `StringDigitSubstitute` | Enum | Specifies how digits are substituted in a string.
+| `StringFormatFlags` | Enum | Specifies formatting options for strings.
+| `StringTrimming` | Enum | Specifies how text is trimmed when it does not fit.
(1) New element (does not belogs to `System.Drawing` library).
+### GeneXus.Drawing.Drawing2D
+Advanced 2D graphics functionalities based on `System.Drawing.Drawing2D` for complex vector graphics and rendering tasks.
+
+| Name | Type | Description
+|-----------------------|----------|--------------
+| `Blend` | Class | Defines a blend of colors along a gradient.
+| `ColorBlend` | Class | Defines a blend of colors for a gradient.
+| `GraphicsPath` | Class | Represents a series of connected lines and curves.
+| `HatchBrush` | Class | Defines a brush with a hatching pattern.
+| `PathGradientBrush` | Class | Defines a brush that fills an area with a gradient of colors.
+| `LinearGradientBrush` | Class | Defines a brush that fills an area with a linear gradient of colors.
+| `Matrix` | Struct | Defines a transformation matrix for graphics operations.
+| `PathData` | Struct | Contains data associated with a GraphicsPath object.
+| `CombineMode` | Enum | Specifies how two graphics objects are combined.
+| `CompositingQuality` | Enum | Specifies the quality of compositing operations.
+| `CoordinateSpace` | Enum | Specifies the coordinate space for transformations.
+| `DashCap` | Enum | Specifies the cap style for dashed lines.
+| `FillMode` | Enum | Specifies the fill mode for filling shapes.
+| `InterpolationMode` | Enum | Specifies the interpolation mode for scaling and resizing.
+| `LineCap` | Enum | Specifies the shape of the end of a line.
+| `LineJoin` | Enum | Specifies the shape used to join two connected lines.
+| `MatrixOrder` | Enum | Specifies the order of matrix transformations.
+| `PenAlignment` | Enum | Specifies the alignment of a pen's stroke.
+| `PenType` | Enum | Specifies the type of pen used for drawing.
+| `PixelOffsetMode` | Enum | Specifies how to offset pixels when drawing.
+| `SmoothingMode` | Enum | Specifies the level of smoothing applied to graphics.
+| `WarpMode` | Enum | Specifies how text is warped.
+| `WrapMode` | Enum | Specifies how text wraps within its container.
+
### GeneXus.Drawing.Imaging
Advanced image processing based on `System.Drawing.Imaging` to support sophisticated image manipulation and format handling.
@@ -114,9 +161,11 @@ Advanced typographic features based on `System.Drawing.Text` for managing and re
| Name | Type | Description
|---------------------------|----------|--------------
| `FontCollection` | Class | Represents a collection of fonts.
-| `InstalledFontCollection` | Class | Represents a collection of installed fonts.
-| `PrivateFontCollection` | Class | Represents a collection of private fonts.
-| `GenericFontFamilies` | Enum | Specifies generic font families.
+| `InstalledFontCollection` | Class | Represents a collection of installed fonts.
+| `PrivateFontCollection` | Class | Represents a collection of private fonts.
+| `GenericFontFamilies` | Enum | Specifies generic font families.
+| `HotkeyPrefix` | Enum | Specifies how hotkey prefixes are rendered.
+| `TextRenderingHint` | Enum | Specifies the level of text rendering quality.
### GeneXus.Drawing.Interop
Advanced interoperability utilities based on `System.Drawing.Interop` that includes definitions used in font management and graphics rendering.
diff --git a/src/Common/Bitmap.cs b/src/Common/Bitmap.cs
index 756e8d7..fe99353 100644
--- a/src/Common/Bitmap.cs
+++ b/src/Common/Bitmap.cs
@@ -53,8 +53,12 @@ public Bitmap(float width, float 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();
+ public Bitmap(int width, int height, Graphics g)
+ : this(width, height)
+ {
+ HorizontalResolution = g.DpiX;
+ VerticalResolution = g.DpiY;
+ }
///
/// Initializes a new instance of the class with the specified size and format.
@@ -119,13 +123,20 @@ 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, PixelFormat format)
+ public object Clone(RectangleF 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();
}
+ ///
+ /// Creates a copy of the section of this defined
+ /// by structure and with a specified PixelFormat enumeration.
+ ///
+ public object Clone(Rectangle rect, PixelFormat format)
+ => Clone(new RectangleF(rect.m_rect), format);
+
#endregion
@@ -253,7 +264,7 @@ protected override void RotateFlip(int degrees, float scaleX, float scaleY)
///
public void SetPixel(int x, int y, Color color)
{
- var c = new SKColor((byte)color.R, (byte)color.G, (byte)color.B, (byte)color.A);
+ var c = new SKColor(color.R, color.G, color.B, color.A);
m_bitmap.SetPixel(x, y, c);
}
diff --git a/src/Common/Brush.cs b/src/Common/Brush.cs
new file mode 100644
index 0000000..9d47420
--- /dev/null
+++ b/src/Common/Brush.cs
@@ -0,0 +1,192 @@
+using System;
+using SkiaSharp;
+
+namespace GeneXus.Drawing;
+
+public abstract class Brush : IDisposable, ICloneable
+{
+ internal readonly SKPaint m_paint;
+
+ internal Brush(SKPaint paint)
+ {
+ m_paint = paint ?? throw new ArgumentNullException(nameof(paint));
+ m_paint.Style = SKPaintStyle.Fill;
+ }
+
+ ///
+ /// Cleans up resources for this .
+ ///
+ ~Brush() => Dispose(false);
+
+
+ #region IDisposable
+
+ ///
+ /// Cleans up resources for this .
+ ///
+ public void Dispose()
+ {
+ GC.SuppressFinalize(this);
+ Dispose(true);
+ }
+
+ protected virtual void Dispose(bool disposing) => m_paint.Dispose();
+
+ #endregion
+
+
+ #region IClonable
+
+ ///
+ /// Creates an exact copy of this .
+ ///
+ public abstract object Clone();
+
+ #endregion
+
+
+ #region Operators
+
+ ///
+ /// Creates a with the coordinates of the specified .
+ ///
+ public static explicit operator SKPaint(Brush brush) => brush.m_paint;
+
+ #endregion
+
+
+ #region Utilities
+
+ protected static Drawing2D.Blend GetBlendTriangularShape(float focus, float scale)
+ {
+ if (focus < 0 || focus > 1)
+ throw new ArgumentException("Invalid focus value", nameof(focus));
+ if (scale < 0 || scale > 1)
+ throw new ArgumentException("Invalid scale value", nameof(scale));
+
+ int count = focus == 0 || focus == 1 ? 2 : 3;
+ Drawing2D.Blend blend = new(count);
+
+ if (focus == 0)
+ {
+ blend.Positions[0] = 0;
+ blend.Factors[1] = scale;
+ blend.Positions[1] = 1;
+ blend.Factors[1] = 0;
+ }
+ else if (focus == 1)
+ {
+ blend.Positions[0] = 0;
+ blend.Factors[1] = 0;
+ blend.Positions[1] = 1;
+ blend.Factors[1] = scale;
+ }
+ else
+ {
+ blend.Positions[0] = 0;
+ blend.Factors[0] = 0;
+ blend.Positions[1] = focus;
+ blend.Factors[1] = scale;
+ blend.Positions[2] = 1;
+ blend.Factors[2] = 0;
+ }
+ return blend;
+ }
+
+ protected static Drawing2D.Blend GetSigmaBellShape(float focus, float scale = 1.0f)
+ {
+ if (focus < 0 || focus > 1)
+ throw new ArgumentException("Invalid focus value", nameof(focus));
+ if (scale < 0 || scale > 1)
+ throw new ArgumentException("Invalid scale value", nameof(scale));
+
+ int count = focus == 0 || focus == 1 ? 256 : 511;
+ Drawing2D.Blend m_blend = new(count);
+
+ // TODO: clear preset colors
+
+ float fallOffLenght = 2.0f;
+ if (focus == 0)
+ {
+ m_blend.Positions[0] = focus;
+ m_blend.Factors[0] = scale;
+
+ SigmaBellBlend(ref m_blend, focus, scale, 1 / fallOffLenght, 1f / 2, 1f / 255, 1, count - 1, true);
+
+ m_blend.Positions[count - 1] = 1f;
+ m_blend.Factors[count - 1] = 0f;
+ }
+ else if (focus == 1)
+ {
+ m_blend.Positions[0] = 0f;
+ m_blend.Factors[0] = 0f;
+
+ SigmaBellBlend(ref m_blend, focus, scale, 1 / fallOffLenght, 1f / 2, 1f / 255, 1, count - 1, false);
+
+ m_blend.Positions[count - 1] = focus;
+ m_blend.Factors[count - 1] = scale;
+ }
+ else
+ {
+ int middle = count / 2;
+
+ // left part of the sigma bell
+ m_blend.Positions[0] = 0f;
+ m_blend.Factors[0] = 0f;
+
+ SigmaBellBlend(ref m_blend, focus, scale, focus / (2 * fallOffLenght), focus / 2, focus / 255, 1, middle, false);
+
+ // middle part of the sigma bell
+ m_blend.Positions[middle] = focus;
+ m_blend.Factors[middle] = scale;
+
+ // right part of the sigma bell
+ SigmaBellBlend(ref m_blend, focus, scale, (1 - focus) / (2 * fallOffLenght), (1 + focus) / 2, (1 - focus) / 255, middle + 1, count - 1, true);
+
+ m_blend.Positions[count - 1] = 1f;
+ m_blend.Factors[count - 1] = 0f;
+ }
+ return m_blend;
+
+ static void SigmaBellBlend(ref Drawing2D.Blend blend, float focus, float scale, float sigma, float mean, float delta, int startIndex, int endIndex, bool invert)
+ {
+ float sg = invert ? -1 : 1;
+ float x0 = invert ? 1f : 0f;
+
+ float cb = (1 + sg * Erf(x0, sigma, mean)) / 2;
+ float ct = (1 + sg * Erf(focus, sigma, mean)) / 2;
+ float ch = ct - cb;
+
+ float offset = invert ? focus : 0;
+ float pos = delta + offset;
+
+ for (int index = startIndex; index < endIndex; index++, pos += delta)
+ {
+ blend.Positions[index] = pos;
+ blend.Factors[index] = scale / ch * ((1 + sg * Erf(pos, sigma, mean)) / 2 - cb);
+ }
+ }
+
+ static float Erf(float x, float sigma, float mean, int terms = 6)
+ {
+ /*
+ * Error function (Erf) for Gaussian distribution by Maclaurin series:
+ * erf (z) = (2 / sqrt (pi)) * infinite sum of [(pow (-1, n) * pow (z, 2n+1))/(n! * (2n+1))]
+ */
+ float constant = 2 / (float)Math.Sqrt(Math.PI);
+ float z = (x - mean) / (sigma * (float)Math.Sqrt(2));
+
+ float series = z;
+ for (int n = 1, fact = 1; n < terms; n++, fact *= n)
+ {
+ int sign = (int)Math.Pow(-1, n);
+ int step = 2 * n + 1;
+ series += sign * (float)Math.Pow(z, step) / (fact * step);
+ }
+
+ return constant * series;
+ }
+ }
+
+ #endregion
+}
diff --git a/src/Common/Brushes.cs b/src/Common/Brushes.cs
new file mode 100644
index 0000000..e96bb40
--- /dev/null
+++ b/src/Common/Brushes.cs
@@ -0,0 +1,168 @@
+
+
+namespace GeneXus.Drawing;
+
+public static class Brushes
+{
+ public static Brush Transparent => new SolidBrush(Color.Transparent);
+
+ public static Brush AliceBlue => new SolidBrush(Color.AliceBlue);
+ public static Brush AntiqueWhite => new SolidBrush(Color.AntiqueWhite);
+ public static Brush Aqua => new SolidBrush(Color.Aqua);
+ public static Brush Aquamarine => new SolidBrush(Color.Aquamarine);
+ public static Brush Azure => new SolidBrush(Color.Azure);
+
+ public static Brush Beige => new SolidBrush(Color.Beige);
+ public static Brush Bisque => new SolidBrush(Color.Bisque);
+ public static Brush Black => new SolidBrush(Color.Black);
+ public static Brush BlanchedAlmond => new SolidBrush(Color.BlanchedAlmond);
+ public static Brush Blue => new SolidBrush(Color.Blue);
+ public static Brush BlueViolet => new SolidBrush(Color.BlueViolet);
+ public static Brush Brown => new SolidBrush(Color.Brown);
+ public static Brush BurlyWood => new SolidBrush(Color.BurlyWood);
+
+ public static Brush CadetBlue => new SolidBrush(Color.CadetBlue);
+ public static Brush Chartreuse => new SolidBrush(Color.Chartreuse);
+ public static Brush Chocolate => new SolidBrush(Color.Chocolate);
+ public static Brush Coral => new SolidBrush(Color.Coral);
+ public static Brush CornflowerBlue => new SolidBrush(Color.CornflowerBlue);
+ public static Brush Cornsilk => new SolidBrush(Color.Cornsilk);
+ public static Brush Crimson => new SolidBrush(Color.Crimson);
+ public static Brush Cyan => new SolidBrush(Color.Cyan);
+
+ public static Brush DarkBlue => new SolidBrush(Color.DarkBlue);
+ public static Brush DarkCyan => new SolidBrush(Color.DarkCyan);
+ public static Brush DarkGoldenrod => new SolidBrush(Color.DarkGoldenrod);
+ public static Brush DarkGray => new SolidBrush(Color.DarkGray);
+ public static Brush DarkGreen => new SolidBrush(Color.DarkGreen);
+ public static Brush DarkKhaki => new SolidBrush(Color.DarkKhaki);
+ public static Brush DarkMagenta => new SolidBrush(Color.DarkMagenta);
+ public static Brush DarkOliveGreen => new SolidBrush(Color.DarkOliveGreen);
+ public static Brush DarkOrange => new SolidBrush(Color.DarkOrange);
+ public static Brush DarkOrchid => new SolidBrush(Color.DarkOrchid);
+ public static Brush DarkRed => new SolidBrush(Color.DarkRed);
+ public static Brush DarkSalmon => new SolidBrush(Color.DarkSalmon);
+ public static Brush DarkSeaGreen => new SolidBrush(Color.DarkSeaGreen);
+ public static Brush DarkSlateBlue => new SolidBrush(Color.DarkSlateBlue);
+ public static Brush DarkSlateGray => new SolidBrush(Color.DarkSlateGray);
+ public static Brush DarkTurquoise => new SolidBrush(Color.DarkTurquoise);
+ public static Brush DarkViolet => new SolidBrush(Color.DarkViolet);
+ public static Brush DeepPink => new SolidBrush(Color.DeepPink);
+ public static Brush DeepSkyBlue => new SolidBrush(Color.DeepSkyBlue);
+ public static Brush DimGray => new SolidBrush(Color.DimGray);
+ public static Brush DodgerBlue => new SolidBrush(Color.DodgerBlue);
+
+ public static Brush Firebrick => new SolidBrush(Color.Firebrick);
+ public static Brush FloralWhite => new SolidBrush(Color.FloralWhite);
+ public static Brush ForestGreen => new SolidBrush(Color.ForestGreen);
+ public static Brush Fuchsia => new SolidBrush(Color.Fuchsia);
+
+ public static Brush Gainsboro => new SolidBrush(Color.Gainsboro);
+ public static Brush GhostWhite => new SolidBrush(Color.GhostWhite);
+ public static Brush Gold => new SolidBrush(Color.Gold);
+ public static Brush Goldenrod => new SolidBrush(Color.Goldenrod);
+ public static Brush Gray => new SolidBrush(Color.Gray);
+ public static Brush Green => new SolidBrush(Color.Green);
+ public static Brush GreenYellow => new SolidBrush(Color.GreenYellow);
+
+ public static Brush Honeydew => new SolidBrush(Color.Honeydew);
+ public static Brush HotPink => new SolidBrush(Color.HotPink);
+
+ public static Brush IndianRed => new SolidBrush(Color.IndianRed);
+ public static Brush Indigo => new SolidBrush(Color.Indigo);
+ public static Brush Ivory => new SolidBrush(Color.Ivory);
+
+ public static Brush Khaki => new SolidBrush(Color.Khaki);
+
+ public static Brush Lavender => new SolidBrush(Color.Lavender);
+ public static Brush LavenderBlush => new SolidBrush(Color.LavenderBlush);
+ public static Brush LawnGreen => new SolidBrush(Color.LawnGreen);
+ public static Brush LemonChiffon => new SolidBrush(Color.LemonChiffon);
+ public static Brush LightBlue => new SolidBrush(Color.LightBlue);
+ public static Brush LightCoral => new SolidBrush(Color.LightCoral);
+ public static Brush LightCyan => new SolidBrush(Color.LightCyan);
+ public static Brush LightGoldenrodYellow => new SolidBrush(Color.LightGoldenrodYellow);
+ public static Brush LightGreen => new SolidBrush(Color.LightGreen);
+ public static Brush LightGray => new SolidBrush(Color.LightGray);
+ public static Brush LightPink => new SolidBrush(Color.LightPink);
+ public static Brush LightSalmon => new SolidBrush(Color.LightSalmon);
+ public static Brush LightSeaGreen => new SolidBrush(Color.LightSeaGreen);
+ public static Brush LightSkyBlue => new SolidBrush(Color.LightSkyBlue);
+ public static Brush LightSlateGray => new SolidBrush(Color.LightSlateGray);
+ public static Brush LightSteelBlue => new SolidBrush(Color.LightSteelBlue);
+ public static Brush LightYellow => new SolidBrush(Color.LightYellow);
+ public static Brush Lime => new SolidBrush(Color.Lime);
+ public static Brush LimeGreen => new SolidBrush(Color.LimeGreen);
+ public static Brush Linen => new SolidBrush(Color.Linen);
+
+ public static Brush Magenta => new SolidBrush(Color.Magenta);
+ public static Brush Maroon => new SolidBrush(Color.Maroon);
+ public static Brush MediumAquamarine => new SolidBrush(Color.MediumAquamarine);
+ public static Brush MediumBlue => new SolidBrush(Color.MediumBlue);
+ public static Brush MediumOrchid => new SolidBrush(Color.MediumOrchid);
+ public static Brush MediumPurple => new SolidBrush(Color.MediumPurple);
+ public static Brush MediumSeaGreen => new SolidBrush(Color.MediumSeaGreen);
+ public static Brush MediumSlateBlue => new SolidBrush(Color.MediumSlateBlue);
+ public static Brush MediumSpringGreen => new SolidBrush(Color.MediumSpringGreen);
+ public static Brush MediumTurquoise => new SolidBrush(Color.MediumTurquoise);
+ public static Brush MediumVioletRed => new SolidBrush(Color.MediumVioletRed);
+ public static Brush MidnightBlue => new SolidBrush(Color.MidnightBlue);
+ public static Brush MintCream => new SolidBrush(Color.MintCream);
+ public static Brush MistyRose => new SolidBrush(Color.MistyRose);
+ public static Brush Moccasin => new SolidBrush(Color.Moccasin);
+
+ public static Brush NavajoWhite => new SolidBrush(Color.NavajoWhite);
+ public static Brush Navy => new SolidBrush(Color.Navy);
+
+ public static Brush OldLace => new SolidBrush(Color.OldLace);
+ public static Brush Olive => new SolidBrush(Color.Olive);
+ public static Brush OliveDrab => new SolidBrush(Color.OliveDrab);
+ public static Brush Orange => new SolidBrush(Color.Orange);
+ public static Brush OrangeRed => new SolidBrush(Color.OrangeRed);
+ public static Brush Orchid => new SolidBrush(Color.Orchid);
+
+ public static Brush PaleGoldenrod => new SolidBrush(Color.PaleGoldenrod);
+ public static Brush PaleGreen => new SolidBrush(Color.PaleGreen);
+ public static Brush PaleTurquoise => new SolidBrush(Color.PaleTurquoise);
+ public static Brush PaleVioletRed => new SolidBrush(Color.PaleVioletRed);
+ public static Brush PapayaWhip => new SolidBrush(Color.PapayaWhip);
+ public static Brush PeachPuff => new SolidBrush(Color.PeachPuff);
+ public static Brush Peru => new SolidBrush(Color.Peru);
+ public static Brush Pink => new SolidBrush(Color.Pink);
+ public static Brush Plum => new SolidBrush(Color.Plum);
+ public static Brush PowderBlue => new SolidBrush(Color.PowderBlue);
+ public static Brush Purple => new SolidBrush(Color.Purple);
+
+ public static Brush Red => new SolidBrush(Color.Red);
+ public static Brush RosyBrown => new SolidBrush(Color.RosyBrown);
+ public static Brush RoyalBlue => new SolidBrush(Color.RoyalBlue);
+
+ public static Brush SaddleBrown => new SolidBrush(Color.SaddleBrown);
+ public static Brush Salmon => new SolidBrush(Color.Salmon);
+ public static Brush SandyBrown => new SolidBrush(Color.SandyBrown);
+ public static Brush SeaGreen => new SolidBrush(Color.SeaGreen);
+ public static Brush SeaShell => new SolidBrush(Color.SeaShell);
+ public static Brush Sienna => new SolidBrush(Color.Sienna);
+ public static Brush Silver => new SolidBrush(Color.Silver);
+ public static Brush SkyBlue => new SolidBrush(Color.SkyBlue);
+ public static Brush SlateBlue => new SolidBrush(Color.SlateBlue);
+ public static Brush SlateGray => new SolidBrush(Color.SlateGray);
+ public static Brush Snow => new SolidBrush(Color.Snow);
+ public static Brush SpringGreen => new SolidBrush(Color.SpringGreen);
+ public static Brush SteelBlue => new SolidBrush(Color.SteelBlue);
+
+ public static Brush Tan => new SolidBrush(Color.Tan);
+ public static Brush Teal => new SolidBrush(Color.Teal);
+ public static Brush Thistle => new SolidBrush(Color.Thistle);
+ public static Brush Tomato => new SolidBrush(Color.Tomato);
+ public static Brush Turquoise => new SolidBrush(Color.Turquoise);
+
+ public static Brush Violet => new SolidBrush(Color.Violet);
+
+ public static Brush Wheat => new SolidBrush(Color.Wheat);
+ public static Brush White => new SolidBrush(Color.White);
+ public static Brush WhiteSmoke => new SolidBrush(Color.WhiteSmoke);
+
+ public static Brush Yellow => new SolidBrush(Color.Yellow);
+ public static Brush YellowGreen => new SolidBrush(Color.YellowGreen);
+}
diff --git a/src/Common/CharacterRange.cs b/src/Common/CharacterRange.cs
new file mode 100644
index 0000000..69dea7a
--- /dev/null
+++ b/src/Common/CharacterRange.cs
@@ -0,0 +1,85 @@
+using System;
+
+namespace GeneXus.Drawing;
+
+public struct CharacterRange : IEquatable
+{
+ public CharacterRange(int first, int length)
+ {
+ First = first;
+ Length = length;
+ }
+
+
+ #region Operators
+
+ ///
+ /// Compares two objects. Gets a value indicating
+ /// whether the and values of the
+ /// two objects are equal.
+ ///
+ public static bool operator ==(CharacterRange cr1, CharacterRange cr2) => cr1.Equals(cr2);
+
+ ///
+ /// Compares two objects. Gets a value indicating
+ /// whether the and values of the
+ /// two objects are not equal.
+ ///
+ public static bool operator !=(CharacterRange cr1, CharacterRange cr2) => !cr1.Equals(cr2);
+
+ #endregion
+
+
+ #region IEqualitable
+
+ ///
+ /// Indicates whether the current instance is equal to another instance of the same type.
+ ///
+ public readonly bool Equals(CharacterRange other)
+ => First == other.First && Length == other.Length;
+
+ ///
+ /// Gets a value indicating whether this object is equivalent to the specified object.
+ ///
+ public override readonly bool Equals(object obj)
+ => obj is CharacterRange other && Equals(other);
+
+ ///
+ /// Returns the hash code for this instance.
+ ///
+ public override readonly int GetHashCode()
+ => Combine(First, Length);
+
+ #endregion
+
+
+ #region Properties
+
+ ///
+ /// Gets or sets the position in the string of the first character of this .
+ ///
+ public int First { get; set; }
+
+ ///
+ /// Gets or sets the number of positions in this .
+ ///
+ public int Length { get; set; }
+
+ #endregion
+
+
+ #region Utilities
+
+ private const uint PRIME1 = 2654435761U, PRIME2 = 2246822519U;
+
+ private static int Combine(params object[] objects)
+ {
+ uint hash = PRIME1;
+ foreach (var obj in objects)
+ hash = hash * PRIME2 + (uint)obj.GetHashCode();
+ return Convert.ToInt32(hash);
+ }
+
+ #endregion
+}
+
diff --git a/src/Common/Color.cs b/src/Common/Color.cs
index a828ab0..08ce15f 100644
--- a/src/Common/Color.cs
+++ b/src/Common/Color.cs
@@ -33,6 +33,9 @@ internal Color(SKColor color, string name = null, int index = 0)
m_index = index;
}
+ private Color(SKColor color, KnownColor knownColor)
+ : this(color, knownColor.ToString(), (int)knownColor) { }
+
///
/// Initializes a new instance of the structure with the specified
/// alpha, red, green and blue values.
@@ -140,22 +143,22 @@ public readonly bool IsSystemColor
///
/// Gets the alpha component value of this structure.
///
- public readonly int A => m_color.Alpha;
+ public readonly byte A => m_color.Alpha;
///
/// Gets the red component value of this structure.
///
- public readonly int R => m_color.Red;
+ public readonly byte R => m_color.Red;
///
/// Gets the green component value of this structure.
///
- public readonly int G => m_color.Green;
+ public readonly byte G => m_color.Green;
///
/// Gets the blue component value of this structure.
///
- public readonly int B => m_color.Blue;
+ public readonly byte B => m_color.Blue;
///
/// Gets the name component value of this structure.
@@ -273,7 +276,7 @@ public static Color FromKnownColor(KnownColor color)
#region Methods
///
- /// Gets the 32-bit ARGB value of this structure.
+ /// Gets the 32-bit ARGB value of this structure.
///
public readonly int ToArgb() => (A << 24) | (R << 16) | (G << 8) | B;
@@ -297,6 +300,20 @@ public static Color FromKnownColor(KnownColor color)
///
public readonly KnownColor ToKnownColor() => Enum.TryParse(Name, out KnownColor color) ? color : 0;
+ ///
+ /// Linearly interpolates two by a given amount clamped between 0 and 1.
+ ///
+ public static Color Blend(Color color1, Color color2, float amount)
+ {
+ if (amount <= 0) return color1;
+ if (amount >= 1) return color2;
+ int r = (int)(color1.R + (color2.R - color1.R) * amount);
+ int g = (int)(color1.G + (color2.G - color1.G) * amount);
+ int b = (int)(color1.B + (color2.B - color1.B) * amount);
+ int a = (int)(color1.A + (color2.A - color1.A) * amount);
+ return FromArgb(a, r, g, b);
+ }
+
#endregion
@@ -365,148 +382,148 @@ private static SKColor CreateFromHex(string hex, bool argb)
#region NamedColors
- 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);
- public static Color Aquamarine => new(SKColors.Aquamarine);
- public static Color Azure => new(SKColors.Azure);
- public static Color Beige => new(SKColors.Beige);
- public static Color Bisque => new(SKColors.Bisque);
- public static Color Black => new(SKColors.Black);
- public static Color BlanchedAlmond => new(SKColors.BlanchedAlmond);
- public static Color Blue => new(SKColors.Blue);
- public static Color BlueViolet => new(SKColors.BlueViolet);
- public static Color Brown => new(SKColors.Brown);
- public static Color BurlyWood => new(SKColors.BurlyWood);
- public static Color CadetBlue => new(SKColors.CadetBlue);
- public static Color Chartreuse => new(SKColors.Chartreuse);
- public static Color Chocolate => new(SKColors.Chocolate);
- public static Color Coral => new(SKColors.Coral);
- public static Color CornflowerBlue => new(SKColors.CornflowerBlue);
- public static Color Cornsilk => new(SKColors.Cornsilk);
- public static Color Crimson => new(SKColors.Crimson);
- public static Color Cyan => new(SKColors.Cyan);
- public static Color DarkBlue => new(SKColors.DarkBlue);
- public static Color DarkCyan => new(SKColors.DarkCyan);
- public static Color DarkGoldenrod => new(SKColors.DarkGoldenrod);
- public static Color DarkGray => new(SKColors.DarkGray);
- public static Color DarkGreen => new(SKColors.DarkGreen);
- public static Color DarkKhaki => new(SKColors.DarkKhaki);
- public static Color DarkMagenta => new(SKColors.DarkMagenta);
- public static Color DarkOliveGreen => new(SKColors.DarkOliveGreen);
- public static Color DarkOrange => new(SKColors.DarkOrange);
- public static Color DarkOrchid => new(SKColors.DarkOrchid);
- public static Color DarkRed => new(SKColors.DarkRed);
- public static Color DarkSalmon => new(SKColors.DarkSalmon);
- public static Color DarkSeaGreen => new(SKColors.DarkSeaGreen);
- public static Color DarkSlateBlue => new(SKColors.DarkSlateBlue);
- public static Color DarkSlateGray => new(SKColors.DarkSlateGray);
- public static Color DarkTurquoise => new(SKColors.DarkTurquoise);
- public static Color DarkViolet => new(SKColors.DarkViolet);
- public static Color DeepPink => new(SKColors.DeepPink);
- public static Color DeepSkyBlue => new(SKColors.DeepSkyBlue);
- public static Color DimGray => new(SKColors.DimGray);
- public static Color DodgerBlue => new(SKColors.DodgerBlue);
- public static Color Firebrick => new(SKColors.Firebrick);
- public static Color FloralWhite => new(SKColors.FloralWhite);
- public static Color ForestGreen => new(SKColors.ForestGreen);
- public static Color Fuchsia => new(SKColors.Fuchsia);
- public static Color Gainsboro => new(SKColors.Gainsboro);
- public static Color GhostWhite => new(SKColors.GhostWhite);
- public static Color Gold => new(SKColors.Gold);
- public static Color Goldenrod => new(SKColors.Goldenrod);
- public static Color Gray => new(SKColors.Gray);
- public static Color Green => new(SKColors.Green);
- public static Color GreenYellow => new(SKColors.GreenYellow);
- public static Color Honeydew => new(SKColors.Honeydew);
- public static Color HotPink => new(SKColors.HotPink);
- public static Color IndianRed => new(SKColors.IndianRed);
- public static Color Indigo => new(SKColors.Indigo);
- public static Color Ivory => new(SKColors.Ivory);
- public static Color Khaki => new(SKColors.Khaki);
- public static Color Lavender => new(SKColors.Lavender);
- public static Color LavenderBlush => new(SKColors.LavenderBlush);
- public static Color LawnGreen => new(SKColors.LawnGreen);
- public static Color LemonChiffon => new(SKColors.LemonChiffon);
- public static Color LightBlue => new(SKColors.LightBlue);
- public static Color LightCoral => new(SKColors.LightCoral);
- public static Color LightCyan => new(SKColors.LightCyan);
- public static Color LightGoldenrodYellow => new(SKColors.LightGoldenrodYellow);
- public static Color LightGray => new(SKColors.LightGray);
- public static Color LightGreen => new(SKColors.LightGreen);
- public static Color LightPink => new(SKColors.LightPink);
- public static Color LightSalmon => new(SKColors.LightSalmon);
- public static Color LightSeaGreen => new(SKColors.LightSeaGreen);
- public static Color LightSkyBlue => new(SKColors.LightSkyBlue);
- public static Color LightSlateGray => new(SKColors.LightSlateGray);
- public static Color LightSteelBlue => new(SKColors.LightSteelBlue);
- public static Color LightYellow => new(SKColors.LightYellow);
- public static Color Lime => new(SKColors.Lime);
- public static Color LimeGreen => new(SKColors.LimeGreen);
- public static Color Linen => new(SKColors.Linen);
- public static Color Magenta => new(SKColors.Magenta);
- public static Color Maroon => new(SKColors.Maroon);
- public static Color MediumAquamarine => new(SKColors.MediumAquamarine);
- public static Color MediumBlue => new(SKColors.MediumBlue);
- public static Color MediumOrchid => new(SKColors.MediumOrchid);
- public static Color MediumPurple => new(SKColors.MediumPurple);
- public static Color MediumSeaGreen => new(SKColors.MediumSeaGreen);
- public static Color MediumSlateBlue => new(SKColors.MediumSlateBlue);
- public static Color MediumSpringGreen => new(SKColors.MediumSpringGreen);
- public static Color MediumTurquoise => new(SKColors.MediumTurquoise);
- public static Color MediumVioletRed => new(SKColors.MediumVioletRed);
- public static Color MidnightBlue => new(SKColors.MidnightBlue);
- public static Color MintCream => new(SKColors.MintCream);
- public static Color MistyRose => new(SKColors.MistyRose);
- public static Color Moccasin => new(SKColors.Moccasin);
- public static Color NavajoWhite => new(SKColors.NavajoWhite);
- public static Color Navy => new(SKColors.Navy);
- public static Color OldLace => new(SKColors.OldLace);
- public static Color Olive => new(SKColors.Olive);
- public static Color OliveDrab => new(SKColors.OliveDrab);
- public static Color Orange => new(SKColors.Orange);
- public static Color OrangeRed => new(SKColors.OrangeRed);
- public static Color Orchid => new(SKColors.Orchid);
- public static Color PaleGoldenrod => new(SKColors.PaleGoldenrod);
- public static Color PaleGreen => new(SKColors.PaleGreen);
- public static Color PaleTurquoise => new(SKColors.PaleTurquoise);
- public static Color PaleVioletRed => new(SKColors.PaleVioletRed);
- public static Color PapayaWhip => new(SKColors.PapayaWhip);
- public static Color PeachPuff => new(SKColors.PeachPuff);
- public static Color Peru => new(SKColors.Peru);
- public static Color Pink => new(SKColors.Pink);
- public static Color Plum => new(SKColors.Plum);
- public static Color PowderBlue => new(SKColors.PowderBlue);
- public static Color Purple => new(SKColors.Purple);
- public static Color Red => new(SKColors.Red);
- public static Color RebeccaPurple => new("#663399");
- public static Color RosyBrown => new(SKColors.RosyBrown);
- public static Color RoyalBlue => new(SKColors.RoyalBlue);
- public static Color SaddleBrown => new(SKColors.SaddleBrown);
- public static Color Salmon => new(SKColors.Salmon);
- public static Color SandyBrown => new(SKColors.SandyBrown);
- public static Color SeaGreen => new(SKColors.SeaGreen);
- public static Color SeaShell => new(SKColors.SeaShell);
- public static Color Sienna => new(SKColors.Sienna);
- public static Color Silver => new(SKColors.Silver);
- public static Color SkyBlue => new(SKColors.SkyBlue);
- public static Color SlateBlue => new(SKColors.SlateBlue);
- public static Color SlateGray => new(SKColors.SlateGray);
- public static Color Snow => new(SKColors.Snow);
- public static Color SpringGreen => new(SKColors.SpringGreen);
- public static Color SteelBlue => new(SKColors.SteelBlue);
- public static Color Tan => new(SKColors.Tan);
- public static Color Teal => new(SKColors.Teal);
- public static Color Thistle => new(SKColors.Thistle);
- public static Color Tomato => new(SKColors.Tomato);
- public static Color Turquoise => new(SKColors.Turquoise);
- public static Color Violet => new(SKColors.Violet);
- public static Color Wheat => new(SKColors.Wheat);
- public static Color White => new(SKColors.White);
- public static Color WhiteSmoke => new(SKColors.WhiteSmoke);
- public static Color Yellow => new(SKColors.Yellow);
- public static Color YellowGreen => new(SKColors.YellowGreen);
+ public static Color Transparent => new(SKColors.Transparent, KnownColor.Transparent);
+ public static Color AliceBlue => new(SKColors.AliceBlue, KnownColor.AliceBlue);
+ public static Color AntiqueWhite => new(SKColors.AntiqueWhite, KnownColor.AntiqueWhite);
+ public static Color Aqua => new(SKColors.Aqua, KnownColor.Aqua);
+ public static Color Aquamarine => new(SKColors.Aquamarine, KnownColor.Aquamarine);
+ public static Color Azure => new(SKColors.Azure, KnownColor.Azure);
+ public static Color Beige => new(SKColors.Beige, KnownColor.Beige);
+ public static Color Bisque => new(SKColors.Bisque, KnownColor.Bisque);
+ public static Color Black => new(SKColors.Black, KnownColor.Black);
+ public static Color BlanchedAlmond => new(SKColors.BlanchedAlmond, KnownColor.BlanchedAlmond);
+ public static Color Blue => new(SKColors.Blue, KnownColor.Blue);
+ public static Color BlueViolet => new(SKColors.BlueViolet, KnownColor.BlueViolet);
+ public static Color Brown => new(SKColors.Brown, KnownColor.Brown);
+ public static Color BurlyWood => new(SKColors.BurlyWood, KnownColor.BurlyWood);
+ public static Color CadetBlue => new(SKColors.CadetBlue, KnownColor.CadetBlue);
+ public static Color Chartreuse => new(SKColors.Chartreuse, KnownColor.Chartreuse);
+ public static Color Chocolate => new(SKColors.Chocolate, KnownColor.Chocolate);
+ public static Color Coral => new(SKColors.Coral, KnownColor.Coral);
+ public static Color CornflowerBlue => new(SKColors.CornflowerBlue, KnownColor.CornflowerBlue);
+ public static Color Cornsilk => new(SKColors.Cornsilk, KnownColor.Cornsilk);
+ public static Color Crimson => new(SKColors.Crimson, KnownColor.Crimson);
+ public static Color Cyan => new(SKColors.Cyan, KnownColor.Cyan);
+ public static Color DarkBlue => new(SKColors.DarkBlue, KnownColor.DarkBlue);
+ public static Color DarkCyan => new(SKColors.DarkCyan, KnownColor.DarkCyan);
+ public static Color DarkGoldenrod => new(SKColors.DarkGoldenrod, KnownColor.DarkGoldenrod);
+ public static Color DarkGray => new(SKColors.DarkGray, KnownColor.DarkGray);
+ public static Color DarkGreen => new(SKColors.DarkGreen, KnownColor.DarkGreen);
+ public static Color DarkKhaki => new(SKColors.DarkKhaki, KnownColor.DarkKhaki);
+ public static Color DarkMagenta => new(SKColors.DarkMagenta, KnownColor.DarkMagenta);
+ public static Color DarkOliveGreen => new(SKColors.DarkOliveGreen, KnownColor.DarkOliveGreen);
+ public static Color DarkOrange => new(SKColors.DarkOrange, KnownColor.DarkOrange);
+ public static Color DarkOrchid => new(SKColors.DarkOrchid, KnownColor.DarkOrchid);
+ public static Color DarkRed => new(SKColors.DarkRed, KnownColor.DarkRed);
+ public static Color DarkSalmon => new(SKColors.DarkSalmon, KnownColor.DarkSalmon);
+ public static Color DarkSeaGreen => new(SKColors.DarkSeaGreen, KnownColor.DarkSeaGreen);
+ public static Color DarkSlateBlue => new(SKColors.DarkSlateBlue, KnownColor.DarkSlateBlue);
+ public static Color DarkSlateGray => new(SKColors.DarkSlateGray, KnownColor.DarkSlateGray);
+ public static Color DarkTurquoise => new(SKColors.DarkTurquoise, KnownColor.DarkTurquoise);
+ public static Color DarkViolet => new(SKColors.DarkViolet, KnownColor.DarkViolet);
+ public static Color DeepPink => new(SKColors.DeepPink, KnownColor.DeepPink);
+ public static Color DeepSkyBlue => new(SKColors.DeepSkyBlue, KnownColor.DeepSkyBlue);
+ public static Color DimGray => new(SKColors.DimGray, KnownColor.DimGray);
+ public static Color DodgerBlue => new(SKColors.DodgerBlue, KnownColor.DodgerBlue);
+ public static Color Firebrick => new(SKColors.Firebrick, KnownColor.Firebrick);
+ public static Color FloralWhite => new(SKColors.FloralWhite, KnownColor.FloralWhite);
+ public static Color ForestGreen => new(SKColors.ForestGreen, KnownColor.ForestGreen);
+ public static Color Fuchsia => new(SKColors.Fuchsia, KnownColor.Fuchsia);
+ public static Color Gainsboro => new(SKColors.Gainsboro, KnownColor.Gainsboro);
+ public static Color GhostWhite => new(SKColors.GhostWhite, KnownColor.GhostWhite);
+ public static Color Gold => new(SKColors.Gold, KnownColor.Gold);
+ public static Color Goldenrod => new(SKColors.Goldenrod, KnownColor.Goldenrod);
+ public static Color Gray => new(SKColors.Gray, KnownColor.Gray);
+ public static Color Green => new(SKColors.Green, KnownColor.Green);
+ public static Color GreenYellow => new(SKColors.GreenYellow, KnownColor.GreenYellow);
+ public static Color Honeydew => new(SKColors.Honeydew, KnownColor.Honeydew);
+ public static Color HotPink => new(SKColors.HotPink, KnownColor.HotPink);
+ public static Color IndianRed => new(SKColors.IndianRed, KnownColor.IndianRed);
+ public static Color Indigo => new(SKColors.Indigo, KnownColor.Indigo);
+ public static Color Ivory => new(SKColors.Ivory, KnownColor.Ivory);
+ public static Color Khaki => new(SKColors.Khaki, KnownColor.Khaki);
+ public static Color Lavender => new(SKColors.Lavender, KnownColor.Lavender);
+ public static Color LavenderBlush => new(SKColors.LavenderBlush, KnownColor.LavenderBlush);
+ public static Color LawnGreen => new(SKColors.LawnGreen, KnownColor.LawnGreen);
+ public static Color LemonChiffon => new(SKColors.LemonChiffon, KnownColor.LemonChiffon);
+ public static Color LightBlue => new(SKColors.LightBlue, KnownColor.LightBlue);
+ public static Color LightCoral => new(SKColors.LightCoral, KnownColor.LightCoral);
+ public static Color LightCyan => new(SKColors.LightCyan, KnownColor.LightCyan);
+ public static Color LightGoldenrodYellow => new(SKColors.LightGoldenrodYellow, KnownColor.LightGoldenrodYellow);
+ public static Color LightGray => new(SKColors.LightGray, KnownColor.LightGray);
+ public static Color LightGreen => new(SKColors.LightGreen, KnownColor.LightGreen);
+ public static Color LightPink => new(SKColors.LightPink, KnownColor.LightPink);
+ public static Color LightSalmon => new(SKColors.LightSalmon, KnownColor.LightSalmon);
+ public static Color LightSeaGreen => new(SKColors.LightSeaGreen, KnownColor.LightSeaGreen);
+ public static Color LightSkyBlue => new(SKColors.LightSkyBlue, KnownColor.LightSkyBlue);
+ public static Color LightSlateGray => new(SKColors.LightSlateGray, KnownColor.LightSlateGray);
+ public static Color LightSteelBlue => new(SKColors.LightSteelBlue, KnownColor.LightSteelBlue);
+ public static Color LightYellow => new(SKColors.LightYellow, KnownColor.LightYellow);
+ public static Color Lime => new(SKColors.Lime, KnownColor.Lime);
+ public static Color LimeGreen => new(SKColors.LimeGreen, KnownColor.LimeGreen);
+ public static Color Linen => new(SKColors.Linen, KnownColor.Linen);
+ public static Color Magenta => new(SKColors.Magenta, KnownColor.Magenta);
+ public static Color Maroon => new(SKColors.Maroon, KnownColor.Maroon);
+ public static Color MediumAquamarine => new(SKColors.MediumAquamarine, KnownColor.MediumAquamarine);
+ public static Color MediumBlue => new(SKColors.MediumBlue, KnownColor.MediumBlue);
+ public static Color MediumOrchid => new(SKColors.MediumOrchid, KnownColor.MediumOrchid);
+ public static Color MediumPurple => new(SKColors.MediumPurple, KnownColor.MediumPurple);
+ public static Color MediumSeaGreen => new(SKColors.MediumSeaGreen, KnownColor.MediumSeaGreen);
+ public static Color MediumSlateBlue => new(SKColors.MediumSlateBlue, KnownColor.MediumSlateBlue);
+ public static Color MediumSpringGreen => new(SKColors.MediumSpringGreen, KnownColor.MediumSpringGreen);
+ public static Color MediumTurquoise => new(SKColors.MediumTurquoise, KnownColor.MediumTurquoise);
+ public static Color MediumVioletRed => new(SKColors.MediumVioletRed, KnownColor.MediumVioletRed);
+ public static Color MidnightBlue => new(SKColors.MidnightBlue, KnownColor.MidnightBlue);
+ public static Color MintCream => new(SKColors.MintCream, KnownColor.MintCream);
+ public static Color MistyRose => new(SKColors.MistyRose, KnownColor.MistyRose);
+ public static Color Moccasin => new(SKColors.Moccasin, KnownColor.Moccasin);
+ public static Color NavajoWhite => new(SKColors.NavajoWhite, KnownColor.NavajoWhite);
+ public static Color Navy => new(SKColors.Navy, KnownColor.Navy);
+ public static Color OldLace => new(SKColors.OldLace, KnownColor.OldLace);
+ public static Color Olive => new(SKColors.Olive, KnownColor.Olive);
+ public static Color OliveDrab => new(SKColors.OliveDrab, KnownColor.OliveDrab);
+ public static Color Orange => new(SKColors.Orange, KnownColor.Orange);
+ public static Color OrangeRed => new(SKColors.OrangeRed, KnownColor.OrangeRed);
+ public static Color Orchid => new(SKColors.Orchid, KnownColor.Orchid);
+ public static Color PaleGoldenrod => new(SKColors.PaleGoldenrod, KnownColor.PaleGoldenrod);
+ public static Color PaleGreen => new(SKColors.PaleGreen, KnownColor.PaleGreen);
+ public static Color PaleTurquoise => new(SKColors.PaleTurquoise, KnownColor.PaleTurquoise);
+ public static Color PaleVioletRed => new(SKColors.PaleVioletRed, KnownColor.PaleVioletRed);
+ public static Color PapayaWhip => new(SKColors.PapayaWhip, KnownColor.PapayaWhip);
+ public static Color PeachPuff => new(SKColors.PeachPuff, KnownColor.PeachPuff);
+ public static Color Peru => new(SKColors.Peru, KnownColor.Peru);
+ public static Color Pink => new(SKColors.Pink, KnownColor.Pink);
+ public static Color Plum => new(SKColors.Plum, KnownColor.Plum);
+ public static Color PowderBlue => new(SKColors.PowderBlue, KnownColor.PowderBlue);
+ public static Color Purple => new(SKColors.Purple, KnownColor.Purple);
+ public static Color Red => new(SKColors.Red, KnownColor.Red);
+ public static Color RebeccaPurple => new(SKColor.Parse("#663399"), KnownColor.RebeccaPurple);
+ public static Color RosyBrown => new(SKColors.RosyBrown, KnownColor.RosyBrown);
+ public static Color RoyalBlue => new(SKColors.RoyalBlue, KnownColor.RoyalBlue);
+ public static Color SaddleBrown => new(SKColors.SaddleBrown, KnownColor.SaddleBrown);
+ public static Color Salmon => new(SKColors.Salmon, KnownColor.Salmon);
+ public static Color SandyBrown => new(SKColors.SandyBrown, KnownColor.SandyBrown);
+ public static Color SeaGreen => new(SKColors.SeaGreen, KnownColor.SeaGreen);
+ public static Color SeaShell => new(SKColors.SeaShell, KnownColor.SeaShell);
+ public static Color Sienna => new(SKColors.Sienna, KnownColor.Sienna);
+ public static Color Silver => new(SKColors.Silver, KnownColor.Silver);
+ public static Color SkyBlue => new(SKColors.SkyBlue, KnownColor.SkyBlue);
+ public static Color SlateBlue => new(SKColors.SlateBlue, KnownColor.SlateBlue);
+ public static Color SlateGray => new(SKColors.SlateGray, KnownColor.SlateGray);
+ public static Color Snow => new(SKColors.Snow, KnownColor.Snow);
+ public static Color SpringGreen => new(SKColors.SpringGreen, KnownColor.SpringGreen);
+ public static Color SteelBlue => new(SKColors.SteelBlue, KnownColor.SteelBlue);
+ public static Color Tan => new(SKColors.Tan, KnownColor.Tan);
+ public static Color Teal => new(SKColors.Teal, KnownColor.Teal);
+ public static Color Thistle => new(SKColors.Thistle, KnownColor.Thistle);
+ public static Color Tomato => new(SKColors.Tomato, KnownColor.Tomato);
+ public static Color Turquoise => new(SKColors.Turquoise, KnownColor.Turquoise);
+ public static Color Violet => new(SKColors.Violet, KnownColor.Violet);
+ public static Color Wheat => new(SKColors.Wheat, KnownColor.Wheat);
+ public static Color White => new(SKColors.White, KnownColor.White);
+ public static Color WhiteSmoke => new(SKColors.WhiteSmoke, KnownColor.WhiteSmoke);
+ public static Color Yellow => new(SKColors.Yellow, KnownColor.Yellow);
+ public static Color YellowGreen => new(SKColors.YellowGreen, KnownColor.YellowGreen);
#endregion
}
diff --git a/src/Common/ColorConverter.cs b/src/Common/ColorConverter.cs
new file mode 100644
index 0000000..53b8934
--- /dev/null
+++ b/src/Common/ColorConverter.cs
@@ -0,0 +1,145 @@
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.ComponentModel.Design.Serialization;
+using System.Globalization;
+using System.Linq;
+using System.Reflection;
+
+namespace GeneXus.Drawing;
+
+///
+/// Converts colors from one data type to another. Access this class through the .
+///
+public class ColorConverter : TypeConverter
+{
+ private static readonly Lazy s_valuesLazy = new(() =>
+ {
+ var set = new HashSet();
+ foreach (PropertyInfo prop in typeof(Color).GetProperties(BindingFlags.Public | BindingFlags.Static))
+ if (prop.GetValue(null) is Color color)
+ set.Add(color);
+ return new StandardValuesCollection(set.OrderBy(c => c, new ColorComparer()).ToList());
+ });
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ColorConverter() { }
+
+
+ # region TypeConverter overrides
+
+ ///
+ /// Determines if this converter can convert an object in the given source type to the native type of the converter.
+ ///
+ public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
+ => sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
+
+ ///
+ /// Returns a value indicating whether this converter can convert an object to the given destination type using the context.
+ ///
+ public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
+ => destinationType == typeof(InstanceDescriptor) || base.CanConvertTo(context, destinationType);
+
+ ///
+ /// Converts the given object to the converter's native type.
+ ///
+ public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
+ => value is string strValue ? ColorTranslator.ConvertFromString(strValue, culture ?? CultureInfo.CurrentCulture)
+ : base.ConvertFrom(context, culture, value);
+
+ ///
+ /// Converts the specified object to another type.
+ ///
+ public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
+ {
+ if (destinationType == null)
+ throw new ArgumentNullException(nameof(destinationType));
+
+ if (value is Color c)
+ {
+ if (destinationType == typeof(string))
+ {
+ if (c.IsEmpty)
+ return string.Empty;
+
+ if (c.IsKnownColor)
+ return c.Name;
+
+ if (c.IsNamedColor)
+ return $"'{c.Name}'";
+
+ culture ??= CultureInfo.CurrentCulture;
+
+ var converter = TypeDescriptor.GetConverter(typeof(int));
+ var components = c.A < 255 ? new[] { c.A, c.R, c.G, c.B } : new[] { c.R, c.G, c.B };
+
+ var sep = string.Concat(culture.TextInfo.ListSeparator, " ");
+ var args = components.Select(comp => converter.ConvertToString(context, culture, comp));
+ return string.Join(sep, args);
+ }
+ else if (destinationType == typeof(InstanceDescriptor))
+ {
+ MemberInfo member = null;
+ object[] args = new object[] { };
+
+ if (c.IsEmpty)
+ {
+ member = typeof(Color).GetField("Empty");
+ }
+ else if (c.IsKnownColor)
+ {
+ member = typeof(Color).GetProperty(c.Name);
+ }
+ else if (c.IsNamedColor)
+ {
+ member = typeof(Color).GetMethod("FromName", new[] { typeof(string) });
+ args = new object[] { c.Name };
+ }
+ else if (c.A < 255)
+ {
+ member = typeof(Color).GetMethod("FromArgb", new[] { typeof(int), typeof(int), typeof(int), typeof(int) });
+ args = new object[] { c.A, c.R, c.G, c.B };
+ }
+ else
+ {
+ member = typeof(Color).GetMethod("FromArgb", new[] { typeof(int), typeof(int), typeof(int) });
+ args = new object[] { c.R, c.G, c.B };
+ }
+
+ if (member != null)
+ return new InstanceDescriptor(member, args);
+ return null;
+ }
+ }
+
+ return base.ConvertTo(context, culture, value, destinationType);
+ }
+
+ ///
+ /// Retrieves a collection containing a set of standard values for the data type for which this validator
+ /// is designed. This will return null if the data type does not support a standard set of values.
+ ///
+ public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
+ => s_valuesLazy.Value;
+
+ ///
+ /// Determines if this object supports a standard set of values that can be chosen from a list.
+ ///
+ public override bool GetStandardValuesSupported(ITypeDescriptorContext context)
+ => true;
+
+ #endregion
+
+
+ #region Utilities
+
+ private sealed class ColorComparer : IComparer
+ {
+ public int Compare(Color left, Color right) => string.CompareOrdinal(left.Name, right.Name);
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/src/Common/ColorTranslator.cs b/src/Common/ColorTranslator.cs
new file mode 100644
index 0000000..ba859bf
--- /dev/null
+++ b/src/Common/ColorTranslator.cs
@@ -0,0 +1,80 @@
+using System;
+using System.Globalization;
+
+namespace GeneXus.Drawing;
+
+///
+/// Translates colors to and from objects.
+///
+public static class ColorTranslator
+{
+ // COLORREF is 0x00BBGGRR
+ private const int R_SHIFT = 0, G_SHIFT = 8, B_SHIFT = 16;
+ private const int OLE_SYS_COLOR_FLAG = unchecked((int)0x80000000);
+
+ #region Methods
+
+ ///
+ /// Translates the specified to an Ole color.
+ ///
+ public static int ToOle(Color c)
+ => c.IsKnownColor && c.IsSystemColor ? throw new NotImplementedException() : ToWin32(c);
+
+ ///
+ /// Translates the specified to an Html string color representation.
+ ///
+ public static string ToHtml(Color c)
+ => c.IsNamedColor ? c.Name : c.Hex;
+
+ ///
+ /// Translates the specified to a Win32 color.
+ ///
+ public static int ToWin32(Color c)
+ => c.R << R_SHIFT | c.G << G_SHIFT | c.B << B_SHIFT;
+
+ ///
+ /// Translates an Html color representation to a .
+ ///
+ public static Color FromHtml(string htmlColor)
+ => ConvertFromString(htmlColor, CultureInfo.CurrentCulture);
+
+ ///
+ /// Translates an Ole color value to a .
+ ///
+ public static Color FromOle(int oleColor)
+ => (oleColor & OLE_SYS_COLOR_FLAG) == 0 ? Color.FromArgb((int)RefToArgb((uint)oleColor)) : throw new NotImplementedException();
+
+ ///
+ /// Translates an Win32 color value to a .
+ ///
+ public static Color FromWin32(int win32Color)
+ => FromOle(win32Color);
+
+ #endregion
+
+
+ #region Utilities
+
+ private static uint RefToArgb(uint value)
+ => ((value >> R_SHIFT) & 0xFF) << 16
+ | ((value >> G_SHIFT) & 0xFF) << 8
+ | ((value >> B_SHIFT) & 0xFF) << 0
+ | 0xFFu << 24;
+
+ internal static Color ConvertFromString(string value, CultureInfo culture)
+ {
+ if (value.StartsWith("#"))
+ return Color.FromHex(value);
+
+ if (value.StartsWith("0x", StringComparison.OrdinalIgnoreCase) || value.StartsWith("&h", StringComparison.OrdinalIgnoreCase))
+ return Color.FromArgb(Convert.ToInt32(value.Substring(2), 16));
+
+ var provider = (NumberFormatInfo)CultureInfo.CurrentCulture.GetFormat(typeof(NumberFormatInfo));
+ if (int.TryParse(value, NumberStyles.Integer, provider, out int argb))
+ return Color.FromArgb(argb);
+
+ return Color.FromName(value.Trim('\''));
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/src/Common/CopyPixelOperation.cs b/src/Common/CopyPixelOperation.cs
new file mode 100644
index 0000000..3b98b89
--- /dev/null
+++ b/src/Common/CopyPixelOperation.cs
@@ -0,0 +1,97 @@
+namespace GeneXus.Drawing;
+
+///
+/// Specifies the Copy Pixel (ROP) operation.
+///
+public enum CopyPixelOperation
+{
+ ///
+ /// Fills the Destination Rectangle using the color associated with the index 0 in the physical palette.
+ /// (This color is black for the default physical palette.)
+ ///
+ Blackness = 0x00000042,
+
+ ///
+ /// Includes any windows that are layered on top.
+ ///
+ CaptureBlt = 0x40000000,
+
+ ///
+ /// The destination area is inverted.
+ ///
+ DestinationInvert = 0x00550009,
+
+ ///
+ /// The colors of the source area are merged with the colors of the selected brush of the destination device
+ /// context using the Boolean AND operator.
+ ///
+ MergeCopy = 0x00C000CA,
+
+ ///
+ /// The colors of the inverted source area are merged with the colors of the destination area by using the Boolean OR operator.
+ ///
+ MergePaint = 0x00BB0226,
+
+ ///
+ /// The bitmap is not mirrored.
+ ///
+ NoMirrorBitmap = unchecked((int)0x80000000),
+
+ ///
+ /// The inverted source area is copied to the destination.
+ ///
+ NotSourceCopy = 0x00330008,
+
+ ///
+ /// The source and destination colors are combined using the Boolean OR operator, and then the resultant color is inverted.
+ ///
+ NotSourceErase = 0x001100A6,
+
+ ///
+ /// The brush currently selected in the destination device context is copied to the destination bitmap.
+ ///
+ PatCopy = 0x00F00021,
+
+ ///
+ /// The colors of the brush currently selected in the destination device context are combined with the colors of
+ /// the destination area using the Boolean XOR operator.
+ ///
+ PatInvert = 0x005A0049,
+
+ ///
+ /// The colors of the brush currently selected in the destination device context are combined with the colors of
+ /// the inverted source area using the Boolean OR operator. The result of this operation is combined with the colors of the destination area using the Boolean OR operator.
+ ///
+ PatPaint = 0x00FB0A09,
+
+ ///
+ /// The colors of the source and destination areas are combined using the Boolean AND operator.
+ ///
+ SourceAnd = 0x008800C6,
+
+ ///
+ /// The source area is copied directly to the destination area.
+ ///
+ SourceCopy = 0x00CC0020,
+
+ ///
+ /// The inverted colors of the destination area are combined with the colors of the source area using the Boolean AND operator.
+ ///
+ SourceErase = 0x00440328,
+
+ ///
+ /// The colors of the source and destination areas are combined using the Boolean XOR operator.
+ ///
+ SourceInvert = 0x00660046,
+
+ ///
+ /// The colors of the source and destination areas are combined using the Boolean OR operator.
+ ///
+ SourcePaint = 0x00EE0086,
+
+ ///
+ /// Fills the destination area using the color associated with index 1 in the physical palette.
+ /// (This color is white for the default physical palette.)
+ ///
+ Whiteness = 0x00FF0062,
+}
\ No newline at end of file
diff --git a/src/Common/Drawing2D/Blend.cs b/src/Common/Drawing2D/Blend.cs
new file mode 100644
index 0000000..86806c3
--- /dev/null
+++ b/src/Common/Drawing2D/Blend.cs
@@ -0,0 +1,28 @@
+namespace GeneXus.Drawing.Drawing2D;
+
+public sealed class Blend
+{
+
+ ///
+ /// Initializes a new instance of the class with the specified number of factors and positions.
+ ///
+ public Blend(int count = 1)
+ {
+ Factors = new float[count];
+ Positions = new float[count];
+ }
+
+ #region Properties
+
+ ///
+ /// Gets or sets an array of blend factors for the gradient.
+ ///
+ public float[] Factors { get; set; }
+
+ ///
+ /// Gets or sets an array of blend positions for the gradient.
+ ///
+ public float[] Positions { get; set; }
+
+ #endregion
+}
diff --git a/src/Common/Drawing2D/ColorBlend.cs b/src/Common/Drawing2D/ColorBlend.cs
new file mode 100644
index 0000000..68da830
--- /dev/null
+++ b/src/Common/Drawing2D/ColorBlend.cs
@@ -0,0 +1,27 @@
+namespace GeneXus.Drawing.Drawing2D;
+
+public sealed class ColorBlend
+{
+ ///
+ /// Initializes a new instance of the class with the specified number of colors and positions.
+ ///
+ public ColorBlend(int count = 1)
+ {
+ Colors = new Color[count];
+ Positions = new float[count];
+ }
+
+ #region Properties
+
+ ///
+ /// Gets or sets an array of colors that represents the colors to use at corresponding positions along a gradient.
+ ///
+ public Color[] Colors { get; set; }
+
+ ///
+ /// Gets or sets the positions along a gradient line.
+ ///
+ public float[] Positions { get; set; }
+
+ #endregion
+}
diff --git a/src/Common/Drawing2D/CombineMode.cs b/src/Common/Drawing2D/CombineMode.cs
new file mode 100644
index 0000000..e7f2120
--- /dev/null
+++ b/src/Common/Drawing2D/CombineMode.cs
@@ -0,0 +1,39 @@
+namespace GeneXus.Drawing.Drawing2D;
+
+///
+/// Specifies how different clipping regions can be combined.
+///
+public enum CombineMode
+{
+ ///
+ /// One clipping region is replaced by another.
+ ///
+ Replace = 0,
+
+ ///
+ /// Two clipping regions are combined by taking their intersection.
+ ///
+ Intersect = 1,
+
+ ///
+ /// Two clipping regions are combined by taking the union of both.
+ ///
+ Union = 2,
+
+ ///
+ /// Two clipping regions are combined by taking only the areas enclosed by one or the other region, but not both.
+ ///
+ Xor = 3,
+
+ ///
+ /// Specifies that the existing region is replaced by the result of the new region being removed from the existing
+ /// region. Said differently, the new region is excluded from the existing region.
+ ///
+ Exclude = 4,
+
+ ///
+ /// Specifies that the existing region is replaced by the result of the existing region being removed from the new
+ /// region. Said differently, the existing region is excluded from the new region.
+ ///
+ Complement = 5
+}
diff --git a/src/Common/Drawing2D/CompositingMode.cs b/src/Common/Drawing2D/CompositingMode.cs
new file mode 100644
index 0000000..8193751
--- /dev/null
+++ b/src/Common/Drawing2D/CompositingMode.cs
@@ -0,0 +1,18 @@
+namespace GeneXus.Drawing.Drawing2D;
+
+///
+/// Specifies how the source colors are combined with the background colors.
+///
+public enum CompositingMode
+{
+ ///
+ /// Specifies that when a color is rendered, it overwrites the background color.
+ ///
+ SourceOver = 0,
+
+ ///
+ /// Specifies that when a color is rendered, it is blended with the background
+ /// color. The blend is determined by the alpha component of the color being rendered.
+ ///
+ SourceCopy = 1
+}
diff --git a/src/Common/Drawing2D/CompositingQuality.cs b/src/Common/Drawing2D/CompositingQuality.cs
new file mode 100644
index 0000000..7785397
--- /dev/null
+++ b/src/Common/Drawing2D/CompositingQuality.cs
@@ -0,0 +1,37 @@
+namespace GeneXus.Drawing.Drawing2D;
+
+///
+/// Specifies how the source colors are combined with the background colors.
+///
+public enum CompositingQuality
+{
+ ///
+ /// Invalid quality.
+ ///
+ Invalid = -1,
+
+ ///
+ /// Default quality.
+ ///
+ Default = 0,
+
+ ///
+ /// High speed, low quality.
+ ///
+ HighSpeed = 1,
+
+ ///
+ /// High quality, low speed compositing.
+ ///
+ HighQuality = 2,
+
+ ///
+ /// Gamma correction is used.
+ ///
+ GammaCorrected = 3,
+
+ ///
+ /// Assume linear values.
+ ///
+ AssumeLinear = 4
+}
diff --git a/src/Common/Drawing2D/CoordinateSpace.cs b/src/Common/Drawing2D/CoordinateSpace.cs
new file mode 100644
index 0000000..9d5b6ad
--- /dev/null
+++ b/src/Common/Drawing2D/CoordinateSpace.cs
@@ -0,0 +1,26 @@
+namespace GeneXus.Drawing.Drawing2D;
+
+///
+/// Specifies the system to use when evaluating coordinates.
+///
+public enum CoordinateSpace
+{
+ ///
+ /// Specifies that coordinates are in the world coordinate context. World coordinates are
+ /// used in a nonphysical environment, such as a modeling environment.
+ ///
+ World = 0,
+
+ ///
+ /// Specifies that coordinates are in the page coordinate context. Their units are defined
+ /// by the property, and must be one of the elements of
+ /// the enumeration.
+ ///
+ Page = 1,
+
+ ///
+ /// Specifies that coordinates are in the device coordinate context. On a computer screen
+ /// the device coordinates are usually measured in pixels.
+ ///
+ Device = 2
+}
diff --git a/src/Common/Drawing2D/DashCap.cs b/src/Common/Drawing2D/DashCap.cs
new file mode 100644
index 0000000..0bc05d2
--- /dev/null
+++ b/src/Common/Drawing2D/DashCap.cs
@@ -0,0 +1,22 @@
+namespace GeneXus.Drawing.Drawing2D;
+
+///
+/// Specifies the available dash cap styles with which a can end a line.
+///
+public enum DashCap
+{
+ ///
+ /// Specifies a square cap that squares off both ends of each dash.
+ ///
+ Flat = 0,
+
+ ///
+ /// Specifies a circular cap that rounds off both ends of each dash.
+ ///
+ Round = 2,
+
+ ///
+ /// Specifies a triangular cap that points both ends of each dash.
+ ///
+ Triangle = 3
+}
diff --git a/src/Common/Drawing2D/FillMode.cs b/src/Common/Drawing2D/FillMode.cs
new file mode 100644
index 0000000..eb44ae0
--- /dev/null
+++ b/src/Common/Drawing2D/FillMode.cs
@@ -0,0 +1,17 @@
+namespace GeneXus.Drawing.Drawing2D;
+
+///
+/// Specifies how the interior of a closed path is filled.
+///
+public enum FillMode
+{
+ ///
+ /// Specifies the alternate fill mode.
+ ///
+ Alternate = 0, // Odd-even fill rule
+
+ ///
+ /// Specifies the winding fill mode.
+ ///
+ Winding = 1, // Non-zero winding fill rule
+}
diff --git a/src/Common/Drawing2D/FlushIntention.cs b/src/Common/Drawing2D/FlushIntention.cs
new file mode 100644
index 0000000..99536cc
--- /dev/null
+++ b/src/Common/Drawing2D/FlushIntention.cs
@@ -0,0 +1,18 @@
+namespace GeneXus.Drawing.Drawing2D;
+
+///
+/// Specifies whether commands in the graphics stack are terminated (flushed) immediately
+/// or executed as soon as possible.
+///
+public enum FlushIntention
+{
+ ///
+ /// Flush all batched rendering operations.
+ ///
+ Flush = 0,
+
+ ///
+ /// Flush all batched rendering operations and wait for them to complete.
+ ///
+ Sync = 1
+}
diff --git a/src/Common/Drawing2D/GraphicsContainer.cs b/src/Common/Drawing2D/GraphicsContainer.cs
new file mode 100644
index 0000000..e30e5ad
--- /dev/null
+++ b/src/Common/Drawing2D/GraphicsContainer.cs
@@ -0,0 +1,7 @@
+namespace GeneXus.Drawing.Drawing2D;
+
+public sealed class GraphicsContainer
+{
+ internal int m_state = -1;
+ internal GraphicsContainer(int state) => m_state = state;
+}
diff --git a/src/Common/Drawing2D/GraphicsPath.cs b/src/Common/Drawing2D/GraphicsPath.cs
new file mode 100644
index 0000000..529eb1f
--- /dev/null
+++ b/src/Common/Drawing2D/GraphicsPath.cs
@@ -0,0 +1,1120 @@
+
+using System;
+using System.Linq;
+using SkiaSharp;
+
+namespace GeneXus.Drawing.Drawing2D;
+
+public sealed class GraphicsPath : ICloneable, IDisposable
+{
+ internal readonly SKPath m_path;
+
+ internal GraphicsPath(SKPath path)
+ {
+ m_path = path;
+ }
+
+ ///
+ /// Initializes a new instance of the class with
+ /// a value of Alternate.
+ ///
+ public GraphicsPath()
+ : this(FillMode.Alternate) { }
+
+ ///
+ /// Initializes a new instance of the class with
+ /// the enumeration.
+ ///
+ public GraphicsPath(FillMode mode)
+ : this(Array.Empty(), Array.Empty(), mode) { }
+
+ ///
+ /// Initializes a new instance of the class with
+ /// the specified and arrays and with the
+ /// specified enumeration element.
+ ///
+ public GraphicsPath(PointF[] points, byte[] types, FillMode mode = FillMode.Alternate)
+ : this(CreatePath(points, types, mode)) { }
+
+ ///
+ /// Initializes a new instance of the class with
+ /// the specified and arrays and with the
+ /// specified enumeration element.
+ ///
+ public GraphicsPath(Point[] points, byte[] types, FillMode mode = FillMode.Alternate)
+ : this(Array.ConvertAll(points, point => new PointF(point.m_point)), types, mode) { }
+
+ ///
+ /// Cleans up resources for this .
+ ///
+ ~GraphicsPath() => Dispose(false);
+
+
+ #region IDisposable
+
+ ///
+ /// Cleans up resources for this .
+ ///
+ public void Dispose()
+ {
+ GC.SuppressFinalize(this);
+ Dispose(true);
+ }
+
+ private void Dispose(bool disposing) => m_path.Dispose();
+
+ #endregion
+
+
+ #region IClonable
+
+ ///
+ /// Creates an exact copy of this .
+ ///
+ public object Clone()
+ {
+ var path = new SKPath(m_path);
+ return new GraphicsPath(path);
+ }
+
+ #endregion
+
+
+ #region Operators
+
+ ///
+ /// Creates a with the coordinates of the specified .
+ ///
+ public static explicit operator SKPath(GraphicsPath path) => path.m_path;
+
+ #endregion
+
+
+ #region Properties
+
+ ///
+ /// Gets or sets a enumeration that determines how the
+ /// interiors of shapes in this are filled.
+ ///
+ public FillMode FillMode
+ {
+ get => m_path.FillType switch
+ {
+ SKPathFillType.EvenOdd => FillMode.Alternate,
+ SKPathFillType.Winding => FillMode.Winding,
+ _ => throw new NotImplementedException($"value {m_path.FillType}")
+ };
+ set => m_path.FillType = value switch
+ {
+ FillMode.Alternate => SKPathFillType.EvenOdd,
+ FillMode.Winding => SKPathFillType.Winding,
+ _ => throw new NotImplementedException($"value {value}")
+ };
+ }
+
+ ///
+ /// Gets a that encapsulates arrays of points and types
+ /// for this .
+ ///
+ public PathData PathData => new()
+ {
+ Points = PathPoints,
+ Types = PathTypes
+ };
+
+ ///
+ /// Gets the points in the path.
+ ///
+ public PointF[] PathPoints => Array.ConvertAll(m_path.Points, point => new PointF(point));
+
+ ///
+ /// Gets the types of the corresponding points in the array.
+ ///
+ public byte[] PathTypes
+ {
+ get
+ {
+ using var iterator = m_path.CreateIterator(false);
+ byte[] types = new byte[m_path.PointCount];
+
+ int index = 0;
+ var points = new SKPoint[4];
+
+ SKPathVerb verb;
+ while ((verb = iterator.Next(points)) != SKPathVerb.Done)
+ {
+ switch (verb)
+ {
+ case SKPathVerb.Move:
+ AddType(1, 0, (byte)PathPointType.Start);
+ break;
+
+ case SKPathVerb.Line:
+ AddType(1, 1, (byte)PathPointType.Line);
+ break;
+
+ case SKPathVerb.Conic:
+ case SKPathVerb.Quad:
+ AddType(2, 1, (byte)PathPointType.Bezier);
+ break;
+
+ case SKPathVerb.Cubic:
+ AddType(3, 1, (byte)PathPointType.Bezier);
+ break;
+
+ case SKPathVerb.Close when index > 0:
+ types[index - 1] |= (byte)PathPointType.CloseSubpath;
+ break;
+ }
+ }
+
+ return types;
+
+ void AddType(int count, int offset, byte type)
+ {
+ if (index >= m_path.PointCount)
+ return;
+ if (points[offset] != m_path.Points[index])
+ return;
+ for (int i = 0; i < count; i++)
+ types[index++] = type;
+ }
+ }
+ }
+
+ ///
+ /// Gets the number of elements in the or the array.
+ ///
+ public int PointCount => m_path.PointCount;
+
+ #endregion
+
+
+ #region Methods
+
+ ///
+ /// Appends an elliptical arc to the current figure bounded
+ /// by a rectangle defined by position and size.
+ ///
+ public void AddArc(int x, int y, int width, int height, float startAngle, float sweepAngle)
+ => AddArc(new Rectangle(x, y, width, height), startAngle, sweepAngle);
+
+ ///
+ /// Appends an elliptical arc to the current figure bounded
+ /// by a structure.
+ ///
+ public void AddArc(Rectangle rect, float startAngle, float sweepAngle)
+ => AddArc(rect.m_rect, startAngle, sweepAngle);
+
+ ///
+ /// Appends an elliptical arc to the current figure bounded
+ /// by a rectangle defined by position and size.
+ ///
+ public void AddArc(float x, float y, float width, float height, float startAngle, float sweepAngle)
+ => AddArc(new RectangleF(x, y, width, height), startAngle, sweepAngle);
+
+ ///
+ /// Appends an elliptical arc to the current figure bounded
+ /// by a structure.
+ ///
+ public void AddArc(RectangleF rect, float startAngle, float sweepAngle)
+ => AddArc(rect.m_rect, startAngle, sweepAngle);
+
+ ///
+ /// Adds a cubic Bézier curve to the current figure defined
+ /// by 4 points' coordinates.
+ ///
+ public void AddBezier(int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4)
+ => AddBezier(new Point(x1, y1), new Point(x2, y2), new Point(x3, y3), new Point(x4, y4));
+
+ ///
+ /// Adds a cubic Bézier curve to the current figure defined
+ /// by 4 structures.
+ ///
+ public void AddBezier(Point pt1, Point pt2, Point pt3, Point pt4)
+ => AddBezier(pt1.m_point, pt2.m_point, pt3.m_point, pt4.m_point);
+
+ ///
+ /// Adds a cubic Bézier curve to the current figure defined
+ /// by 4 points' coordinates.
+ ///
+ public void AddBezier(float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4)
+ => AddBezier(new PointF(x1, y1), new PointF(x2, y2), new PointF(x3, y3), new PointF(x4, y4));
+
+ ///
+ /// Adds a cubic Bézier curve to the current figure defined
+ /// by 4 structures.
+ ///
+ public void AddBezier(PointF pt1, PointF pt2, PointF pt3, PointF pt4)
+ => AddBezier(pt1.m_point, pt2.m_point, pt3.m_point, pt4.m_point);
+
+ ///
+ /// Adds a sequence of connected cubic Bézier curves defined by an
+ /// array of structure to the current figure.
+ ///
+ public void AddBeziers(params PointF[] points)
+ => AddBeziers(Array.ConvertAll(points, point => point.m_point));
+
+ ///
+ /// Adds a sequence of connected cubic Bézier curves defined by an
+ /// array of structure to the current figure.
+ ///
+ public void AddBeziers(params Point[] points)
+ => AddBeziers(Array.ConvertAll(points, point => point.m_point));
+
+ ///
+ /// Adds a closed curve to this path defined by a sequence
+ /// of structure. A cardinal spline
+ /// curve is used because the curve travels through each
+ /// of the points in the array.
+ ///
+ public void AddClosedCurve(params PointF[] points)
+ => AddClosedCurve(points, 0.5f);
+
+ ///
+ /// Adds a closed curve to this path defined by an array
+ /// of structure.
+ ///
+ public void AddClosedCurve(PointF[] points, float tension = 0.5f)
+ => AddCurve(Array.ConvertAll(points, point => point.m_point), tension, true);
+
+ ///
+ /// Adds a closed curve to this path defined by a sequence
+ /// of structure. A cardinal spline
+ /// curve is used because the curve travels through each
+ /// of the points in the array.
+ ///
+ public void AddClosedCurve(params Point[] points)
+ => AddClosedCurve(points);
+
+ ///
+ /// Adds a closed curve to this path defined by an array
+ /// of structure.
+ ///
+ public void AddClosedCurve(Point[] points, float tension = 0.5f)
+ => AddCurve(Array.ConvertAll(points, point => point.m_point), tension, true);
+
+ ///
+ /// Adds a spline curve to the current figure defined
+ /// by a sequence of structures. A
+ /// cardinal spline curve is used because the curve
+ /// travels through each of the points in the array.
+ ///
+ public void AddCurve(params PointF[] points)
+ => AddCurve(points, 0.5f);
+
+ ///
+ /// Adds a spline curve to the current figure defined
+ /// by an array of structure.
+ ///
+ public void AddCurve(PointF[] points, float tension = 0.5f)
+ => AddCurve(points, 0, points.Length - 1, tension);
+
+ ///
+ /// Adds a spline curve to the current figure defined
+ /// by an array of structure but taking
+ /// a certain number of segments starting from an offset.
+ ///
+ public void AddCurve(PointF[] points, int offset, int numberOfSegments, float tension = 0.5f)
+ => AddCurve(points.Skip(offset).Take(numberOfSegments + 1).Select(p => p.m_point).ToArray(), tension, false);
+
+ ///
+ /// Adds a spline curve to the current figure defined
+ /// by a sequence of structures. A
+ /// cardinal spline curve is used because the curve
+ /// travels through each of the points in the array.
+ ///
+ public void AddCurve(params Point[] points)
+ => AddCurve(points, 0.5f);
+
+ ///
+ /// Adds a spline curve to the current figure defined
+ /// by an array of structure.
+ ///
+ public void AddCurve(Point[] points, float tension = 0.5f)
+ => AddCurve(points, 0, points.Length - 1, tension);
+
+ ///
+ /// Adds a spline curve to the current figure defined
+ /// by an array of structure but taking
+ /// a certain number of segments starting from an offset.
+ ///
+ public void AddCurve(Point[] points, int offset, int numberOfSegments, float tension = 0.5f)
+ => AddCurve(points.Skip(offset).Take(numberOfSegments + 1).Select(p => p.m_point).ToArray(), tension, false);
+
+ ///
+ /// Adds an ellipse to the current path bounded
+ /// by a rectangle defined by position and size.
+ ///
+ public void AddEllipse(float x, float y, float width, float height)
+ => AddEllipse(new RectangleF(x, y, width, height));
+
+ ///
+ /// Adds an ellipse to the current path bounded by
+ /// a structure.
+ ///
+ public void AddEllipse(RectangleF rect)
+ => AddEllipse(rect.m_rect);
+
+ ///
+ /// Adds an ellipse to the current path bounded
+ /// by a rectangle defined by position and size.
+ ///
+ public void AddEllipse(int x, int y, int width, int height)
+ => AddEllipse(new Rectangle(x, y, width, height));
+
+ ///
+ /// Adds an ellipse to the current path bounded by
+ /// a structure.
+ ///
+ public void AddEllipse(Rectangle rect)
+ => AddEllipse(rect.m_rect);
+
+ ///
+ /// Appends a line segment to the current figure defined by
+ /// the coordinates of the starting and ending points.
+ ///
+ public void AddLine(float x1, float y1, float x2, float y2)
+ => AddLine(new PointF(x1, y1), new PointF(x2, y2));
+
+ ///
+ /// Appends a line segment to the current figure defined by
+ /// the starting and ending structures.
+ ///
+ public void AddLine(PointF pt1, PointF pt2)
+ => AddLine(pt1.m_point, pt2.m_point);
+
+ ///
+ /// Appends a line segment to the current figure defined by
+ /// the coordinates of the starting and ending points.
+ ///
+ public void AddLine(int x1, int y1, int x2, int y2)
+ => AddLine(new Point(x1, y1), new Point(x2, y2));
+
+ ///
+ /// Appends a line segment to the current figure defined by
+ /// the starting and ending structures.
+ ///
+ public void AddLine(Point pt1, Point pt2)
+ => AddLine(pt1.m_point, pt2.m_point);
+
+ ///
+ /// Appends a series of connected line segments to the end
+ /// of the current figure defined by a sequence of structures.
+ ///
+ public void AddLines(params PointF[] points)
+ => Array.ForEach(Enumerable.Range(0, points.Length - 1).ToArray(), i => AddLine(points[i], points[i + 1]));
+
+ ///
+ /// Appends a series of connected line segments to the end
+ /// of the current figure defined by a sequence of structures.
+ ///
+ public void AddLines(params Point[] points)
+ => Array.ForEach(Enumerable.Range(0, points.Length - 2).ToArray(), i => AddLine(points[i], points[i + 1]));
+
+ ///
+ /// Appends the specified to this path.
+ ///
+ public void AddPath(GraphicsPath path, bool connect)
+ => m_path.AddPath(path.m_path, connect ? SKPathAddMode.Append : SKPathAddMode.Extend);
+
+ ///
+ /// Adds the outline of a pie shape to this path bounded
+ /// by a rectangle defined by position and size.
+ ///
+ public void AddPie(float x, float y, float width, float height, float startAngle, float sweepAngle)
+ => AddPie(new RectangleF(x, y, width, height), startAngle, sweepAngle);
+
+ ///
+ /// Adds the outline of a pie shape to this path bounded
+ /// by a structure.
+ ///
+ public void AddPie(RectangleF rect, float startAngle, float sweepAngle)
+ => AddPie(rect.m_rect, startAngle, sweepAngle);
+
+ ///
+ /// Adds the outline of a pie shape to this path bounded
+ /// by a rectangle defined by position and size.
+ ///
+ public void AddPie(int x, int y, int width, int height, float startAngle, float sweepAngle)
+ => AddPie(new Rectangle(x, y, width, height), startAngle, sweepAngle);
+
+ ///
+ /// Adds the outline of a pie shape to this path bounded
+ /// by a structure.
+ ///
+ public void AddPie(Rectangle rect, float startAngle, float sweepAngle)
+ => AddPie(rect.m_rect, startAngle, sweepAngle);
+
+ ///
+ /// Adds a polygon to this path defined by an array of structures.
+ ///
+ public void AddPolygon(PointF[] points)
+ => AddPolygon(Array.ConvertAll(points, point => point.m_point));
+
+ ///
+ /// Adds a polygon to this path defined by an array of structures.
+ ///
+ public void AddPolygon(Point[] points)
+ => AddPolygon(Array.ConvertAll(points, point => point.m_point));
+
+ ///
+ /// Adds a structure to this path.
+ ///
+ public void AddRectangle(RectangleF rect)
+ => AddRectangle(rect.m_rect);
+
+ ///
+ /// Adds a structure to this path.
+ ///
+ public void AddRectangle(Rectangle rect)
+ => AddRectangle(rect.m_rect);
+
+ ///
+ /// Adds a series of structures to this path.
+ ///
+ public void AddRectangles(params RectangleF[] rects)
+ => Array.ForEach(rects, AddRectangle);
+
+ ///
+ /// Adds a series of structures to this path.
+ ///
+ public void AddRectangles(params Rectangle[] rects)
+ => Array.ForEach(rects, AddRectangle);
+
+ ///
+ /// Adds a text string to this path bounded by structure.
+ ///
+ public void AddString(string text, FontFamily family, int style, float emSize, RectangleF layout, StringFormat format)
+ => AddString(text, family, style, emSize, layout.m_rect, format);
+
+ ///
+ /// Adds a text string to this path bounded by structure.
+ ///
+ public void AddString(string text, FontFamily family, int style, float emSize, Rectangle layout, StringFormat format)
+ => AddString(text, family, style, emSize, layout.m_rect, format);
+
+ ///
+ /// Adds a text string to this path starting from a structure.
+ ///
+ public void AddString(string text, FontFamily family, int style, float emSize, PointF origin, StringFormat format)
+ => AddString(text, family, style, emSize, new RectangleF(origin, float.PositiveInfinity, float.PositiveInfinity), format);
+
+ ///
+ /// Adds a text string to this path starting from a structure.
+ ///
+ public void AddString(string text, FontFamily family, int style, float emSize, Point origin, StringFormat format)
+ => AddString(text, family, style, emSize, new PointF(origin.m_point), format);
+
+ ///
+ /// Clears all markers from this path.
+ ///
+ public void ClearMarkers()
+ => throw new NotSupportedException("skia unsupported feature");
+
+ ///
+ /// Closes all open figures in this path and starts a new figure. It closes
+ /// each open figure by connecting a line from its endpoint to its starting point.
+ ///
+ public void CloseAllFigures()
+ => throw new NotSupportedException("skia unsupported feature");
+
+ ///
+ /// Closes the current figure and starts a new figure. If the current figure
+ /// contains a sequence of connected lines and curves, the method closes the
+ /// loop by connecting a line from the endpoint to the starting point.
+ ///
+ public void CloseFigure()
+ => m_path.Close();
+
+ ///
+ /// Converts each curve in this path into a sequence of connected line segments.
+ ///
+ public void Flatten()
+ => Flatten(new Matrix());
+
+ ///
+ /// Applies the specified transform and then converts each curve
+ /// in this into a sequence of connected
+ /// line segments.
+ ///
+ public void Flatten(Matrix matrix, float flatness = 0.25f)
+ {
+ if (flatness <= 0)
+ throw new ArgumentException($"zero or negative value {flatness}", nameof(flatness));
+
+ if (m_path.PointCount == 0)
+ return;
+
+ var data = PathData;
+ matrix?.TransformPoints(data.Points);
+
+ using var path = new GraphicsPath(data.Points, data.Types, FillMode);
+
+ Reset();
+
+ for (int i = 0; i < path.PointCount; i++)
+ {
+ var type = path.PathTypes[i] & (byte)PathPointType.PathTypeMask;
+ switch((PathPointType)type)
+ {
+ case PathPointType.Start:
+ m_path.MoveTo(path.PathPoints[i].m_point);
+ break;
+
+ case PathPointType.Line:
+ m_path.LineTo(path.PathPoints[i].m_point);
+ break;
+
+ case PathPointType.Bezier:
+ if (i + 2 >= path.PointCount
+ || (path.PathTypes[i + 1] & (byte)PathPointType.PathTypeMask) != (byte)PathPointType.Bezier
+ || (path.PathTypes[i + 2] & (byte)PathPointType.PathTypeMask) != (byte)PathPointType.Bezier)
+ throw new ArgumentException("invalid Bezier curve definition");
+
+ var pt0 = path.PathPoints[i - 1].m_point;
+ var pt1 = path.PathPoints[i + 0].m_point;
+ var pt2 = path.PathPoints[i + 1].m_point;
+ var pt3 = path.PathPoints[i + 2].m_point;
+
+ float d1 = SKPoint.Distance(pt0, pt1);
+ float d2 = SKPoint.Distance(pt1, pt2);
+ float d3 = SKPoint.Distance(pt2, pt3);
+
+ int count = (int)Math.Max(1, d1 + d2 + d3);
+ var lastPoint = pt0;
+ for (int offset = 1; offset < count; offset++)
+ {
+ float t = (offset + 1f) / count;
+ float u = 1f - t;
+
+ float u2 = u * u;
+ float u3 = u * u2;
+ float t2 = t * t;
+ float t3 = t * t2;
+
+ float x = u3 * pt0.X + 3 * t * u2 * pt1.X + 3 * t2 * u * pt2.X + t3 * pt3.X;
+ float y = u3 * pt0.Y + 3 * t * u2 * pt1.Y + 3 * t2 * u * pt2.Y + t3 * pt3.Y;
+
+ var currPoint = new SKPoint(x, y);
+ if (SKPoint.Distance(lastPoint, currPoint) > 1 - flatness)
+ {
+ m_path.LineTo(currPoint);
+ lastPoint = currPoint;
+ }
+ }
+
+ i += 2;
+ break;
+
+ default:
+ throw new NotImplementedException($"point type 0x{type:X2} is not supported at index {i}.");
+ }
+
+ if ((path.PathTypes[i] & (byte)PathPointType.CloseSubpath) == (byte)PathPointType.CloseSubpath)
+ m_path.Close();
+ }
+ }
+
+ ///
+ /// Returns a rectangle that bounds this .
+ ///
+ public RectangleF GetBounds()
+ => GetBounds(new Matrix());
+
+ ///
+ /// Returns a rectangle that bounds this
+ /// when this path is transformed by the specified .
+ ///
+ public RectangleF GetBounds(Matrix matrix)
+ => GetBounds(matrix, new Pen(Color.Transparent, 0));
+
+ ///
+ /// Returns a rectangle that bounds this
+ /// when the current path is transformed by the specified and
+ /// drawn with the specified .
+ ///
+ public RectangleF GetBounds(Matrix matrix, Pen pen)
+ {
+ using var path = new SKPath(m_path);
+ using var transformed = new GraphicsPath(path);
+ transformed.Transform(matrix);
+ using var fill = pen.m_paint.GetFillPath(transformed.m_path) ?? transformed.m_path;
+ return new RectangleF(fill.Bounds);
+ }
+
+ ///
+ /// Gets the last point in the array of this .
+ ///
+ public PointF GetLastPoint()
+ => new(m_path.LastPoint);
+
+ ///
+ /// Indicates whether the specified point is contained within (under) the outline of
+ /// this when drawn with the specified and
+ /// using the specified .
+ ///
+ public bool IsOutlineVisible(float x, float y, Pen pen, Graphics g = null)
+ => IsOutlineVisible(new PointF(x, y), pen, g);
+
+ ///
+ /// Indicates whether the specified structure is contained
+ /// within (under) the outline of this when drawn
+ /// with the specified and using the specified .
+ ///
+ public bool IsOutlineVisible(PointF point, Pen pen, Graphics g = null)
+ => IsOutlineVisible(point.m_point, pen.m_paint, g?.m_canvas.LocalClipBounds);
+
+ ///
+ /// Indicates whether the specified point is contained within (under) the outline of
+ /// this when drawn with the specified and
+ /// using the specified .
+ ///
+ public bool IsOutlineVisible(int x, int y, Pen pen, Graphics g = null)
+ => IsOutlineVisible(new Point(x, y), pen, g);
+
+ ///
+ /// Indicates whether the specified structure is contained
+ /// within (under) the outline of this when drawn
+ /// with the specified and using the specified .
+ ///
+ public bool IsOutlineVisible(Point point, Pen pen, Graphics g = null)
+ => IsOutlineVisible(point.m_point, pen.m_paint, g?.m_canvas.LocalClipBounds);
+
+ ///
+ /// Indicates whether the specified point's coordinate is contained
+ /// within this .
+ ///
+ public bool IsVisible(float x, float y, Graphics g = null)
+ => IsVisible(new PointF(x, y), g);
+
+ ///
+ /// Indicates whether the specified structure is
+ /// contained within this .
+ ///
+ public bool IsVisible(PointF point, Graphics g = null)
+ => IsVisible(point.m_point, g?.m_canvas.LocalClipBounds);
+
+ ///
+ /// Indicates whether the specified point's coordinate is contained
+ /// within this .
+ ///
+ public bool IsVisible(int x, int y, Graphics g = null)
+ => IsVisible(new Point(x, y), g);
+
+ ///
+ /// Indicates whether the specified structure is
+ /// contained within this .
+ ///
+ public bool IsVisible(Point point, Graphics g = null)
+ => IsVisible(point.m_point, g?.m_canvas.LocalClipBounds);
+
+ ///
+ /// Empties the and arrays
+ /// and sets the to .
+ ///
+ public void Reset()
+ => m_path.Reset();
+
+ ///
+ /// Reverses the order of points in the array of this .
+ ///
+ public void Reverse()
+ {
+ using var path = new SKPath(m_path);
+ m_path.Reset();
+ m_path.AddPathReverse(path);
+ }
+
+ ///
+ /// Sets a marker on this .
+ ///
+ public void SetMarkers()
+ => throw new NotImplementedException();
+
+ ///
+ /// Starts a new figure without closing the current figure. All subsequent
+ /// points added to the path are added to this new figure.
+ ///
+ public void StartFigure()
+ => throw new NotImplementedException();
+
+ ///
+ /// Applies a transform matrix to this .
+ ///
+ public void Transform(Matrix matrix)
+ => m_path.Transform(matrix.m_matrix);
+
+ ///
+ /// Applies a warp transform to this , defined by a ,
+ /// a parallelogram (serie of structure), a , and a flatness
+ /// value for curves.
+ ///
+ public void Warp(PointF[] destPoints, RectangleF srcRect, Matrix matrix = null, WarpMode warpMode = WarpMode.Perspective, float flatness = 0.25f)
+ => throw new NotImplementedException();
+
+ ///
+ /// Replaces this with curves that enclose the area that is
+ /// filled when this path is drawn by the specified and flatness
+ /// value for curves.
+ ///
+ public void Widen(Pen pen, Matrix matrix = null, float flatness = 0.25f)
+ => throw new NotImplementedException();
+
+ #endregion
+
+
+ #region Extras
+
+ ///
+ /// Returns the SVG string associated with this .
+ ///
+ public string ToSvg()
+ => m_path.ToSvgPathData();
+
+ ///
+ /// Returns the associated with SVG input string.
+ ///
+ public static GraphicsPath FromSvg(string svg)
+ => new(SKPath.ParseSvgPathData(svg));
+
+ #endregion
+
+
+ #region Helpers
+
+ private void AddArc(SKRect rect, float startAngle, float sweepAngle)
+ => m_path.AddArc(rect, startAngle, sweepAngle);
+
+
+ private void AddBezier(SKPoint pt1, SKPoint pt2, SKPoint pt3, SKPoint pt4)
+ {
+ if (m_path.LastPoint != pt1)
+ m_path.MoveTo(pt1);
+ m_path.CubicTo(pt2, pt3, pt4);
+ }
+
+
+ private void AddBeziers(params SKPoint[] points)
+ {
+ if (points.Length % 4 != 0)
+ throw new ArgumentException($"beziers requires points lenght with multiple of 4", nameof(points));
+ for (int i = 0; i < points.Length; i += 4)
+ AddBezier(points[i], points[i + 1], points[i + 2], points[i + 3]);
+ }
+
+
+ private void AddCurve(SKPoint[] points, float tension, bool closed)
+ {
+ if (points.Length < 2)
+ throw new ArgumentException("At least two points are required", nameof(points));
+
+ tension = Math.Max(0, tension);
+
+ if (m_path.LastPoint != points[0])
+ m_path.MoveTo(points[0]);
+
+ if (points.Length == 2)
+ {
+ m_path.LineTo(points[1]);
+ return;
+ }
+
+ // calculate and add cubic bézier curves
+ for (int i = 0; i < points.Length - 1; i++)
+ {
+ SKPoint p0 = points[i];
+ SKPoint p3 = points[i + 1];
+
+ SKPoint p1, p2;
+
+ if (i == 0)
+ {
+ // first segment
+ p1 = new(p0.X + (closed ? p0.X - p3.X : p3.X - p0.X) * tension / 3, p0.Y + (p3.Y - p0.Y) * tension / 3);
+ }
+ else
+ {
+ SKPoint pPrev = points[i - 1];
+ p1 = new(p0.X + (p3.X - pPrev.X) * tension / 3, p0.Y + (p3.Y - pPrev.Y) * tension / 3);
+ }
+
+ if (i == points.Length - 2)
+ {
+ // last segment
+ p2 = new(p3.X - (closed ? p0.X - p3.X : p3.X - p0.X) * tension / 3, p3.Y - (p3.Y - p0.Y) * tension / 3);
+ }
+ else
+ {
+ SKPoint pNext = points[i + 2];
+ p2 = new(p3.X - (pNext.X - p0.X) * tension / 3, p3.Y - (pNext.Y - p0.Y) * tension / 3);
+ }
+
+ // add cubic bézier curve
+ m_path.CubicTo(p1, p2, p3);
+ }
+
+ if (closed)
+ {
+ // Close the path by adding a segment from the last point to the first point
+ SKPoint p0 = points[points.Length - 1];
+ SKPoint p3 = points[0];
+
+ // Calculate control points for the closing segment
+ SKPoint pPrev = points[points.Length - 2];
+ SKPoint p1 = new(p0.X - (p0.X - pPrev.X) * tension / 3, p0.Y + (p0.Y - pPrev.Y) * tension / 3);
+ SKPoint p2 = new(p3.X - (pPrev.X - p0.X) * tension / 3, p3.Y - (pPrev.Y - p0.Y) * tension / 3);
+
+ // add the closing cubic bézier curve and close path
+ m_path.CubicTo(p1, p2, p3);
+ m_path.Close();
+ }
+ }
+
+
+ private void AddEllipse(SKRect rect)
+ {
+ m_path.AddOval(rect);
+ m_path.Close();
+ }
+
+
+ private void AddLine(SKPoint pt1, SKPoint pt2)
+ {
+ if (m_path.LastPoint != pt1)
+ m_path.MoveTo(pt1);
+ m_path.LineTo(pt2);
+ }
+
+
+ private void AddPie(SKRect rect, float startAngle, float sweepAngle)
+ {
+ m_path.ArcTo(rect, startAngle, sweepAngle, false);
+ m_path.LineTo(rect.MidX, rect.MidY);
+ m_path.Close();
+ }
+
+
+ private void AddPolygon(SKPoint[] points)
+ {
+ if (points.Length < 3)
+ throw new ArgumentException("At least three points are required.");
+ m_path.AddPoly(points, true);
+ m_path.Close();
+ }
+
+
+ private void AddRectangle(SKRect rect)
+ {
+ m_path.AddRect(rect);
+ m_path.Close();
+ }
+
+
+ private void AddString(string text, FontFamily family, int style, float emSize, SKRect layout, StringFormat format)
+ {
+ if (string.IsNullOrEmpty(text)) return;
+ format ??= new StringFormat(StringFormatFlags.NoWrap | StringFormatFlags.NoClip);
+
+ bool isRightToLeft = format.FormatFlags.HasFlag(StringFormatFlags.DirectionRightToLeft);
+
+ var typeface = family.GetTypeface((FontStyle)style);
+ using var font = new SKFont(typeface, (int)emSize);
+
+ using var paint = new SKPaint(font)
+ {
+ TextAlign = format.Alignment switch
+ {
+ StringAlignment.Near => isRightToLeft ? SKTextAlign.Right : SKTextAlign.Left,
+ StringAlignment.Far => isRightToLeft ? SKTextAlign.Left : SKTextAlign.Right,
+ StringAlignment.Center => SKTextAlign.Center,
+ _ => throw new ArgumentException($"invalid {format.Alignment} text alignment.", nameof(format))
+ },
+ Style = SKPaintStyle.Stroke
+ };
+
+ // apply format to the string
+ text = format.ApplyDirection(text);
+ text = format.ApplyTabStops(text);
+ text = format.ApplyControlEscape(text);
+ text = format.ApplyDigitSubstitution(text);
+ text = format.ApplyWrapping(text, layout, paint.MeasureText);
+ text = format.ApplyTrimming(text, layout, paint.FontSpacing, paint.MeasureText);
+ text = format.ApplyHotkey(text, out var underlines);
+
+ // calculate line and underline vertical sizes (https://fiddle.skia.org/i/b5b76e0a15da0c3530071186a9006498_raster.png)
+ float lineHeight = paint.FontMetrics.Descent - paint.FontMetrics.Ascent + paint.FontMetrics.Leading;
+ float underlineOffset = paint.FontMetrics.UnderlinePosition ?? 1.8f;
+ float underlineHeight = paint.FontMetrics.UnderlineThickness ?? paint.GetTextPath("_", 0, 0).Bounds.Height;
+
+ underlineOffset -= underlineHeight / 2;
+
+ // create returning path
+ var path = new SKPath();
+
+ // get path for current text, including breaklines and underlines
+ float lineHeightOffset = 1f, lineWidthOffset = 2f; // NOTE: these values were gotten from trial & error
+ int lineIndexOffset = 0;
+ foreach (string textLine in text.Split(StringFormat.BREAKLINES, StringSplitOptions.None))
+ {
+ // check if the line fits within the layout height
+ if (format.FormatFlags.HasFlag(StringFormatFlags.LineLimit) && lineHeightOffset + lineHeight > layout.Height)
+ break;
+
+ // get text path for current line
+ var textPath = paint.GetTextPath(textLine, 0, 0);
+
+ // adjust horizontal alignments for right-to-left text
+ float rtlOffsetX = isRightToLeft ? layout.Width - paint.MeasureText(textLine) - lineWidthOffset : 0;
+ textPath.Offset(rtlOffsetX, 0);
+
+ // get rect path for each underline defined by hotkey prefix
+ float underlineTop = (int)(lineHeightOffset + underlineOffset);
+ foreach (var underlineIndex in underlines.Where(idx => idx >= lineIndexOffset && idx < lineIndexOffset + textLine.Length))
+ {
+ int relativeIndex = underlineIndex - lineIndexOffset;
+
+ float origin = paint.MeasureText(textLine.Substring(0, relativeIndex));
+ float length = paint.MeasureText(textLine.Substring(relativeIndex, 1));
+
+ var underline = new SKRect(
+ origin + rtlOffsetX,
+ underlineTop,
+ origin + length + rtlOffsetX,
+ underlineTop + underlineHeight);
+ textPath.AddRect(underline);
+ }
+
+ // translate path
+ textPath.Offset(
+ -textPath.Bounds.Left + lineWidthOffset + rtlOffsetX,
+ -paint.FontMetrics.Ascent + lineHeightOffset);
+
+ // add line path
+ path.AddPath(textPath);
+
+ // update offsets
+ lineIndexOffset += textLine.Length + "\n".Length;
+ lineHeightOffset += lineHeight;
+ }
+
+ // align path vertically
+ if (format.LineAlignment == StringAlignment.Center)
+ path.Offset(0, (layout.Height - lineHeightOffset) / 2);
+ else if (format.LineAlignment == StringAlignment.Far)
+ path.Offset(0, layout.Height - lineHeightOffset);
+
+ // apply fit if required
+ if (format.FormatFlags.HasFlag(StringFormatFlags.FitBlackBox))
+ {
+ float fitOffsetX = isRightToLeft
+ ? Math.Min(0, layout.Right - path.Bounds.Right)
+ : Math.Max(0, layout.Left - path.Bounds.Left);
+ path.Offset(fitOffsetX - lineWidthOffset, 0);
+ }
+
+ // apply rotation and offset if required
+ if (format.FormatFlags.HasFlag(StringFormatFlags.DirectionVertical))
+ {
+ path.Transform(SKMatrix.CreateRotationDegrees(90));
+ float rotOffsetX = path.Bounds.Standardized.Height + underlineOffset + underlineHeight;
+ path.Offset(rotOffsetX, 0);
+ }
+
+ // apply clip if required
+ if (!format.FormatFlags.HasFlag(StringFormatFlags.NoClip) && layout.Width > 0 && layout.Height > 0)
+ {
+ var clip = new SKPath();
+ clip.AddRect(new SKRect(
+ Math.Max(layout.Left, path.TightBounds.Left),
+ Math.Max(layout.Top, path.TightBounds.Top),
+ Math.Min(layout.Right, path.TightBounds.Right),
+ Math.Min(layout.Bottom, path.TightBounds.Bottom)));
+ path = path.Op(clip, SKPathOp.Intersect);
+ }
+
+ // add text path to this path
+ m_path.AddPath(path);
+ }
+
+
+ private bool IsOutlineVisible(SKPoint point, SKPaint pen, SKRect? bounds)
+ {
+ bool isBoundContained = bounds?.Contains(point) ?? true;
+ var path = pen.GetFillPath(m_path) ?? m_path;
+ return isBoundContained && path.Contains(point.X, point.Y);
+ }
+
+
+ private bool IsVisible(SKPoint point, SKRect? bounds)
+ {
+ bool isBoundContained = bounds?.Contains(point) ?? m_path.Bounds.Contains(point.X, point.Y);
+ return isBoundContained && m_path.Contains(point.X + 0.1f, point.Y + 0.1f); // NOTE: use an offset becase Skia does not consider the path border
+ }
+
+ #endregion
+
+
+ #region Utilities
+
+ private static SKPath CreatePath(PointF[] points, byte[] types, FillMode mode)
+ {
+ if (points.Length != types.Length)
+ throw new ArgumentException("points and types arrays must have the same length.");
+
+ var path = new SKPath()
+ {
+ FillType = mode switch
+ {
+ FillMode.Alternate => SKPathFillType.EvenOdd,
+ FillMode.Winding => SKPathFillType.Winding,
+ _ => throw new ArgumentException($"invlid value {mode}.", nameof(mode))
+ }
+ };
+
+ for (int i = 0; i < points.Length; i++)
+ {
+ var type = types[i] & (byte)PathPointType.PathTypeMask;
+ switch ((PathPointType)type)
+ {
+ case PathPointType.Start:
+ path.MoveTo(points[i].m_point);
+ break;
+
+ case PathPointType.Line:
+ path.LineTo(points[i].m_point);
+ break;
+
+ case PathPointType.Bezier:
+ if (i + 2 < points.Length
+ && (types[i + 1] & (byte)PathPointType.PathTypeMask) == (byte)PathPointType.Bezier
+ && (types[i + 2] & (byte)PathPointType.PathTypeMask) == (byte)PathPointType.Bezier)
+ {
+ path.CubicTo(points[i].m_point, points[i + 1].m_point, points[i + 2].m_point);
+ i += 2;
+ break;
+ }
+ if (i + 1 < points.Length
+ && (types[i + 1] & (byte)PathPointType.PathTypeMask) == (byte)PathPointType.Bezier)
+ {
+ path.QuadTo(points[i].m_point, points[i + 1].m_point);
+ i += 1;
+ break;
+ }
+ throw new ArgumentException("invalid Bezier curve definition.");
+
+ default:
+ throw new ArgumentException($"unknown type 0x{type:X2} at index {i}", nameof(types));
+ }
+
+ if ((types[i] & (byte)PathPointType.CloseSubpath) == (byte)PathPointType.CloseSubpath)
+ path.Close();
+
+ // TODO: consider PathPointType.PathMarker, PathPointType.DashMode
+ }
+
+ return path;
+ }
+
+ #endregion
+}
diff --git a/src/Common/Drawing2D/GraphicsState.cs b/src/Common/Drawing2D/GraphicsState.cs
new file mode 100644
index 0000000..f76dd0c
--- /dev/null
+++ b/src/Common/Drawing2D/GraphicsState.cs
@@ -0,0 +1,11 @@
+using SkiaSharp;
+using System;
+
+namespace GeneXus.Drawing.Drawing2D;
+
+public sealed class GraphicsState : MarshalByRefObject
+{
+ internal int m_index;
+
+ internal GraphicsState(int state) => m_index = state;
+}
\ No newline at end of file
diff --git a/src/Common/Drawing2D/HatchBrush.cs b/src/Common/Drawing2D/HatchBrush.cs
new file mode 100644
index 0000000..2b644db
--- /dev/null
+++ b/src/Common/Drawing2D/HatchBrush.cs
@@ -0,0 +1,158 @@
+using System;
+using System.Collections.Generic;
+using SkiaSharp;
+
+namespace GeneXus.Drawing.Drawing2D;
+
+public sealed class HatchBrush : Brush
+{
+ private readonly Color m_fore;
+ private readonly Color m_back;
+ private readonly HatchStyle m_style;
+
+ ///
+ /// Initializes a new instance of the class with the
+ /// specified enumeration, foreground color, and background color.
+ ///
+ public HatchBrush(HatchStyle hatchStyle, Color foreColor, Color backColor)
+ : base(new SKPaint { IsDither = true })
+ {
+ m_fore = foreColor;
+ m_back = backColor;
+ m_style = hatchStyle;
+
+ UpdateShader(() => { });
+ }
+
+ ///
+ /// Initializes a new instance of the class with the
+ /// specified enumeration and foreground color.
+ ///
+ public HatchBrush(HatchStyle hatchStyle, Color foreColor)
+ : this(hatchStyle, foreColor, Color.Transparent) { }
+
+ #region IClonable
+
+ ///
+ /// Creates an exact copy of this .
+ ///
+ public override object Clone()
+ => new HatchBrush(m_style, m_fore, m_back);
+
+ #endregion
+
+
+ #region Properties
+
+ ///
+ /// Gets the color of spaces between the hatch lines drawn by this object.
+ ///
+ public Color BackgroundColor => m_back;
+
+ ///
+ /// Gets the color of hatch lines drawn by this object.
+ ///
+ public Color ForegroundColor => m_fore;
+
+ ///
+ /// Gets the hatch style of this object.
+ ///
+ public HatchStyle HatchStyle => m_style;
+
+ #endregion
+
+
+ #region Utilities
+
+ private static readonly Dictionary HATCH_DATA = new()
+ {
+ { HatchStyle.Horizontal, (8, new uint[] { 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }) },
+ { HatchStyle.Vertical, (8, new uint[] { 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80 }) },
+ { HatchStyle.ForwardDiagonal, (8, new uint[] { 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 }) },
+ { HatchStyle.BackwardDiagonal, (8, new uint[] { 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80 }) },
+ { HatchStyle.Cross, (8, new uint[] { 0xFF, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80 }) },
+ { HatchStyle.DiagonalCross, (8, new uint[] { 0x81, 0x42, 0x24, 0x18, 0x18, 0x24, 0x42, 0x81 }) },
+ { HatchStyle.Percent05, (8, new uint[] { 0x80, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00 }) },
+ { HatchStyle.Percent10, (8, new uint[] { 0x80, 0x00, 0x08, 0x00 }) },
+ { HatchStyle.Percent20, (4, new uint[] { 0x08, 0x00, 0x02, 0x00 }) },
+ { HatchStyle.Percent25, (4, new uint[] { 0x08, 0x02 }) },
+ { HatchStyle.Percent30, (4, new uint[] { 0x0A, 0x04, 0x0A, 0x01 }) },
+ { HatchStyle.Percent40, (8, new uint[] { 0xAA, 0x55, 0xAA, 0x51, 0xAA, 0x55, 0xAA, 0x15 }) },
+ { HatchStyle.Percent50, (2, new uint[] { 0x02, 0x01 }) },
+ { HatchStyle.Percent60, (4, new uint[] { 0x0E, 0x05, 0x0B, 0x05 }) },
+ { HatchStyle.Percent70, (4, new uint[] { 0x07, 0x0D }) },
+ { HatchStyle.Percent75, (4, new uint[] { 0x07, 0x0F, 0x0D, 0x0F }) },
+ { HatchStyle.Percent80, (8, new uint[] { 0xEF, 0xFF, 0x7E, 0xFF }) },
+ { HatchStyle.Percent90, (8, new uint[] { 0xFF, 0xFF, 0xFF, 0xF7, 0xFF, 0xFF, 0xFF, 0x7F }) },
+ { HatchStyle.LightDownwardDiagonal, (4, new uint[] { 0x08, 0x04, 0x02, 0x01 }) },
+ { HatchStyle.LightUpwardDiagonal, (4, new uint[] { 0x01, 0x02, 0x04, 0x08 }) },
+ { HatchStyle.DarkDownwardDiagonal, (4, new uint[] { 0x0C, 0x06, 0x03, 0x09 }) },
+ { HatchStyle.DarkUpwardDiagonal, (4, new uint[] { 0x03, 0x06, 0x0C, 0x09 }) },
+ { HatchStyle.WideDownwardDiagonal, (8, new uint[] { 0xC1, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x83 }) },
+ { HatchStyle.WideUpwardDiagonal, (8, new uint[] { 0x83, 0x07, 0x0E, 0x1C, 0x38, 0x70, 0xE0, 0xC1 }) },
+ { HatchStyle.LightVertical, (4, new uint[] { 0x08, 0x08, 0x08, 0x08 }) },
+ { HatchStyle.LightHorizontal, (4, new uint[] { 0x0F, 0x00, 0x00, 0x00 }) },
+ { HatchStyle.NarrowVertical, (2, new uint[] { 0x01, 0x01 }) },
+ { HatchStyle.NarrowHorizontal, (2, new uint[] { 0x03, 0x00 }) },
+ { HatchStyle.DarkVertical, (4, new uint[] { 0x0C, 0x0C, 0x0C, 0x0C }) },
+ { HatchStyle.DarkHorizontal, (4, new uint[] { 0x0F, 0x0F, 0x00, 0x00 }) },
+ { HatchStyle.DashedDownwardDiagonal, (4, new uint[] { 0x00, 0x00, 0x08, 0x04, 0x02, 0x01, 0x00, 0x00 }) },
+ { HatchStyle.DashedUpwardDiagonal, (4, new uint[] { 0x00, 0x00, 0x01, 0x02, 0x04, 0x08, 0x00, 0x00 }) },
+ { HatchStyle.DashedHorizontal, (8, new uint[] { 0xF0, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00 }) },
+ { HatchStyle.DashedVertical, (8, new uint[] { 0x80, 0x80, 0x80, 0x80, 0x08, 0x08, 0x08, 0x08 }) },
+ { HatchStyle.SmallConfetti, (8, new uint[] { 0x80, 0x08, 0x40, 0x03, 0x10, 0x01, 0x20, 0x04 }) },
+ { HatchStyle.LargeConfetti, (8, new uint[] { 0xB1, 0x30, 0x03, 0x1B, 0xD8, 0xC0, 0x06, 0x8D }) },
+ { HatchStyle.ZigZag, (8, new uint[] { 0x81, 0x42, 0x24, 0x18 }) },
+ { HatchStyle.Wave, (8, new uint[] { 0x00, 0x18, 0x25, 0xC0 }) },
+ { HatchStyle.DiagonalBrick, (8, new uint[] { 0x01, 0x02, 0x04, 0x08, 0x18, 0x24, 0x42, 0x81 }) },
+ { HatchStyle.HorizontalBrick, (8, new uint[] { 0xFF, 0x80, 0x80, 0x80, 0xFF, 0x08, 0x08, 0x08 }) },
+ { HatchStyle.Weave, (8, new uint[] { 0x88, 0x54, 0x22, 0x45, 0x88, 0x14, 0x22, 0x51 }) },
+ { HatchStyle.Plaid, (8, new uint[] { 0xAA, 0x55, 0xAA, 0x55, 0xF0, 0xF0, 0xF0, 0xF0 }) },
+ { HatchStyle.Divot, (8, new uint[] { 0x00, 0x10, 0x08, 0x10, 0x00, 0x80, 0x01, 0x80 }) },
+ { HatchStyle.DottedGrid, (8, new uint[] { 0xAA, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00 }) },
+ { HatchStyle.DottedDiamond, (8, new uint[] { 0x80, 0x00, 0x22, 0x00, 0x08, 0x00, 0x22, 0x00 }) },
+ { HatchStyle.Shingle, (8, new uint[] { 0x03, 0x84, 0x48, 0x30, 0x0C, 0x02, 0x01, 0x01 }) },
+ { HatchStyle.Trellis, (4, new uint[] { 0x0F, 0x06, 0x0F, 0x09 }) },
+ { HatchStyle.Sphere, (8, new uint[] { 0x77, 0x89, 0x8F, 0x8F, 0x77, 0x98, 0xF8, 0xF8 }) },
+ { HatchStyle.SmallGrid, (4, new uint[] { 0x0F, 0x08, 0x08, 0x08 }) },
+ { HatchStyle.SmallCheckerBoard, (4, new uint[] { 0x09, 0x06, 0x06, 0x09 }) },
+ { HatchStyle.LargeCheckerBoard, (8, new uint[] { 0xF0, 0xF0, 0xF0, 0xF0, 0x0F, 0x0F, 0x0F, 0x0F }) },
+ { HatchStyle.OutlinedDiamond, (8, new uint[] { 0x82, 0x44, 0x28, 0x10, 0x28, 0x44, 0x82, 0x01 }) },
+ { HatchStyle.SolidDiamond, (8, new uint[] { 0x10, 0x38, 0x7C, 0xFE, 0x7C, 0x38, 0x10, 0x00 }) }
+ };
+
+ private void UpdateShader(Action action)
+ {
+ action();
+
+ if (!HATCH_DATA.TryGetValue(m_style, out var data))
+ throw new NotImplementedException($"hatch style {m_style}.");
+
+ using var bitmap = new SKBitmap(data.Width, data.Pattern.Length);
+ using var canvas = new SKCanvas(bitmap);
+ using var paint = new SKPaint
+ {
+ Color = m_fore.m_color,
+ Style = SKPaintStyle.Fill,
+ IsAntialias = false
+ };
+
+ canvas.Clear(m_back.m_color);
+
+ for (int x = 0; x < data.Width; x++)
+ {
+ for (int y = 0; y < data.Pattern.Length; y++)
+ {
+ int offset = data.Width - x;
+ uint mask = 1u << offset - 1;
+ if ((data.Pattern[y] & mask) == mask)
+ canvas.DrawPoint(x, y, paint);
+ }
+ }
+
+ m_paint.Shader = SKShader.CreateBitmap(bitmap, SKShaderTileMode.Repeat, SKShaderTileMode.Repeat);
+ }
+
+
+ #endregion
+}
diff --git a/src/Common/Drawing2D/HatchStyle.cs b/src/Common/Drawing2D/HatchStyle.cs
new file mode 100644
index 0000000..6c1b399
--- /dev/null
+++ b/src/Common/Drawing2D/HatchStyle.cs
@@ -0,0 +1,287 @@
+namespace GeneXus.Drawing.Drawing2D;
+
+///
+/// Specifies the different patterns available for HatchBrush objects.
+///
+public enum HatchStyle
+{
+ ///
+ /// A pattern of horizontal lines.
+ ///
+ Horizontal = 0,
+
+ ///
+ /// A pattern of vertical lines.
+ ///
+ Vertical = 1,
+
+ ///
+ /// A pattern of lines on a diagonal from upper left to lower right.
+ ///
+ ForwardDiagonal = 2,
+
+ ///
+ /// A pattern of lines on a diagonal from upper right to lower left.
+ ///
+ BackwardDiagonal = 3,
+
+ ///
+ /// Specifies horizontal and vertical lines that cross.
+ ///
+ Cross = 4,
+
+ ///
+ /// A pattern of crisscross diagonal lines.
+ ///
+ DiagonalCross = 5,
+
+ ///
+ /// Specifies a 5-percent hatch. The ratio of foreground color to background color is 5:95.
+ ///
+ Percent05 = 6,
+
+ ///
+ /// Specifies a 10-percent hatch. The ratio of foreground color to background color is 10:90.
+ ///
+ Percent10 = 7,
+
+ ///
+ /// Specifies a 20-percent hatch. The ratio of foreground color to background color is 20:80.
+ ///
+ Percent20 = 8,
+
+ ///
+ /// Specifies a 25-percent hatch. The ratio of foreground color to background color is 25:75.
+ ///
+ Percent25 = 9,
+
+ ///
+ /// Specifies a 30-percent hatch. The ratio of foreground color to background color is 30:70.
+ ///
+ Percent30 = 10,
+
+ ///
+ /// Specifies a 40-percent hatch. The ratio of foreground color to background color is 40:60.
+ ///
+ Percent40 = 11,
+
+ ///
+ /// Specifies a 50-percent hatch. The ratio of foreground color to background color is 50:50.
+ ///
+ Percent50 = 12,
+
+ ///
+ /// Specifies a 60-percent hatch. The ratio of foreground color to background color is 60:40.
+ ///
+ Percent60 = 13,
+
+ ///
+ /// Specifies a 70-percent hatch. The ratio of foreground color to background color is 70:30.
+ ///
+ Percent70 = 14,
+
+ ///
+ /// Specifies a 75-percent hatch. The ratio of foreground color to background color is 75:25.
+ ///
+ Percent75 = 15,
+
+ ///
+ /// Specifies a 80-percent hatch. The ratio of foreground color to background color is 80:20.
+ ///
+ Percent80 = 16,
+
+ ///
+ /// Specifies a 90-percent hatch. The ratio of foreground color to background color is 90:10.
+ ///
+ Percent90 = 17,
+
+ ///
+ /// Specifies diagonal lines that slant to the right from top points to bottom points and are spaced 50 percent closer together than ForwardDiagonal, but are not antialiased.
+ ///
+ LightDownwardDiagonal = 18,
+
+ ///
+ /// Specifies diagonal lines that slant to the left from top points to bottom points and are spaced 50 percent closer together than BackwardDiagonal, but they are not antialiased.
+ ///
+ LightUpwardDiagonal = 19,
+
+ ///
+ /// Specifies diagonal lines that slant to the right from top points to bottom points, are spaced 50 percent closer together than, and are twice the width of ForwardDiagonal. This hatch pattern is not antialiased.
+ ///
+ DarkDownwardDiagonal = 20,
+
+ ///
+ /// Specifies diagonal lines that slant to the left from top points to bottom points, are spaced 50 percent closer together than BackwardDiagonal, and are twice its width, but the lines are not antialiased.
+ ///
+ DarkUpwardDiagonal = 21,
+
+ ///
+ /// Specifies diagonal lines that slant to the right from top points to bottom points, have the same spacing as hatch style ForwardDiagonal, and are triple its width, but are not antialiased.
+ ///
+ WideDownwardDiagonal = 22,
+
+ ///
+ /// Specifies diagonal lines that slant to the left from top points to bottom points, have the same spacing as hatch style BackwardDiagonal, and are triple its width, but are not antialiased.
+ ///
+ WideUpwardDiagonal = 23,
+
+ ///
+ /// Specifies vertical lines that are spaced 50 percent closer together than Vertical.
+ ///
+ LightVertical = 24,
+
+ ///
+ /// Specifies horizontal lines that are spaced 50 percent closer together than Horizontal.
+ ///
+ LightHorizontal = 25,
+
+ ///
+ /// Specifies vertical lines that are spaced 75 percent closer together than hatch style Vertical (or 25 percent closer together than LightVertical).
+ ///
+ NarrowVertical = 26,
+
+ ///
+ /// Specifies horizontal lines that are spaced 75 percent closer together than hatch style Horizontal (or 25 percent closer together than LightHorizontal).
+ ///
+ NarrowHorizontal = 27,
+
+ ///
+ /// Specifies vertical lines that are spaced 50 percent closer together than Vertical and are twice its width.
+ ///
+ DarkVertical = 28,
+
+ ///
+ /// Specifies horizontal lines that are spaced 50 percent closer together than Horizontal and are twice the width of Horizontal.
+ ///
+ DarkHorizontal = 29,
+
+ ///
+ /// Specifies dashed diagonal lines, that slant to the right from top points to bottom points.
+ ///
+ DashedDownwardDiagonal = 30,
+
+ ///
+ /// Specifies dashed diagonal lines, that slant to the left from top points to bottom points.
+ ///
+ DashedUpwardDiagonal = 31,
+
+ ///
+ /// Specifies dashed horizontal lines.
+ ///
+ DashedHorizontal = 32,
+
+ ///
+ /// Specifies dashed vertical lines.
+ ///
+ DashedVertical = 33,
+
+ ///
+ /// Specifies a hatch that has the appearance of confetti.
+ ///
+ SmallConfetti = 34,
+
+ ///
+ /// Specifies a hatch that has the appearance of confetti, and is composed of larger pieces than SmallConfetti.
+ ///
+ LargeConfetti = 35,
+
+ ///
+ /// Specifies horizontal lines that are composed of zigzags.
+ ///
+ ZigZag = 36,
+
+ ///
+ /// Specifies horizontal lines that are composed of tildes.
+ ///
+ Wave = 37,
+
+ ///
+ /// Specifies a hatch that has the appearance of layered bricks that slant to the left from top points to bottom points.
+ ///
+ DiagonalBrick = 38,
+
+ ///
+ /// Specifies a hatch that has the appearance of horizontally layered bricks.
+ ///
+ HorizontalBrick = 39,
+
+ ///
+ /// Specifies a hatch that has the appearance of a woven material.
+ ///
+ Weave = 40,
+
+ ///
+ /// Specifies a hatch that has the appearance of a plaid material.
+ ///
+ Plaid = 41,
+
+ ///
+ /// Specifies a hatch that has the appearance of divots.
+ ///
+ Divot = 42,
+
+ ///
+ /// Specifies horizontal and vertical lines, each of which is composed of dots, that cross.
+ ///
+ DottedGrid = 43,
+
+ ///
+ /// Specifies forward diagonal and backward diagonal lines, each of which is composed of dots, that cross.
+ ///
+ DottedDiamond = 44,
+
+ ///
+ /// Specifies a hatch that has the appearance of diagonally layered shingles that slant to the right from top points to bottom points.
+ ///
+ Shingle = 45,
+
+ ///
+ /// Specifies a hatch that has the appearance of a trellis.
+ ///
+ Trellis = 46,
+
+ ///
+ /// Specifies a hatch that has the appearance of spheres laid adjacent to one another.
+ ///
+ Sphere = 47,
+
+ ///
+ /// Specifies horizontal and vertical lines that cross and are spaced 50 percent closer together than hatch style Cross.
+ ///
+ SmallGrid = 48,
+
+ ///
+ /// Specifies a hatch that has the appearance of a checkerboard.
+ ///
+ SmallCheckerBoard = 49,
+
+ ///
+ /// Specifies a hatch that has the appearance of a checkerboard with squares that are twice the size of SmallCheckerBoard.
+ ///
+ LargeCheckerBoard = 50,
+
+ ///
+ /// Specifies forward diagonal and backward diagonal lines that cross but are not antialiased.
+ ///
+ OutlinedDiamond = 51,
+
+ ///
+ /// Specifies a hatch that hasthe appearance of a checkerboard placed diagonally.
+ ///
+ SolidDiamond = 52,
+
+ ///
+ /// Specifies the hatch style Cross.
+ ///
+ LargeGrid = Cross,
+
+ ///
+ /// Specifies hatch style Horizontal.
+ ///
+ Min = Horizontal,
+
+ ///
+ /// Specifies hatch style LargeGrid.
+ ///
+ Max = LargeGrid
+}
diff --git a/src/Common/Drawing2D/InterpolationMode.cs b/src/Common/Drawing2D/InterpolationMode.cs
new file mode 100644
index 0000000..22a9444
--- /dev/null
+++ b/src/Common/Drawing2D/InterpolationMode.cs
@@ -0,0 +1,56 @@
+namespace GeneXus.Drawing.Drawing2D;
+
+///
+/// Specifies the algorithm that is used when images are scaled or rotated.
+///
+public enum InterpolationMode
+{
+ ///
+ /// Specifies an invalid mode.
+ ///
+ Invalid = -1,
+
+ ///
+ /// Specifies default mode.
+ ///
+ Default = 0,
+
+ ///
+ /// Specifies low quality interpolation.
+ ///
+ Low = 1,
+
+ ///
+ /// Specifies high quality interpolation.
+ ///
+ High = 2,
+
+ ///
+ /// Specifies bilinear interpolation. No prefiltering is done. This mode is not
+ /// suitable for shrinking an image below 50 percent of its original size.
+ ///
+ Bilinear = 3,
+
+ ///
+ /// Specifies bicubic interpolation. No prefiltering is done. This mode is not
+ /// suitable for shrinking an image below 25 percent of its original size.
+ ///
+ Bicubic = 4,
+
+ ///
+ /// Specifies nearest-neighbor interpolation.
+ ///
+ NearestNeighbor = 5,
+
+ ///
+ /// Specifies high-quality, bilinear interpolation. Prefiltering is performed to
+ /// ensure high-quality shrinking.
+ ///
+ HighQualityBilinear = 6,
+
+ ///
+ /// Specifies high-quality, bicubic interpolation. Prefiltering is performed to
+ /// ensure high-quality shrinking. This mode produces the highest quality transformed images.
+ ///
+ HighQualityBicubic = 7
+}
diff --git a/src/Common/Drawing2D/LineCap.cs b/src/Common/Drawing2D/LineCap.cs
new file mode 100644
index 0000000..14c84c5
--- /dev/null
+++ b/src/Common/Drawing2D/LineCap.cs
@@ -0,0 +1,19 @@
+namespace GeneXus.Drawing.Drawing2D;
+
+///
+/// Specifies the available cap styles with which a object can end a line.
+///
+public enum LineCap
+{
+ Flat = 0,
+ Square = 1,
+ Round = 2,
+ Triangle = 3,
+ NoAnchor = 16, // corresponds to flat cap
+ SquareAnchor = 17, // corresponds to square cap
+ RoundAnchor = 18, // corresponds to round cap
+ DiamondAnchor = 19, // corresponds to triangle cap
+ ArrowAnchor = 20, // no correspondence
+ Custom = 255, // custom cap
+ AnchorMask = 40 // mask to check for anchor or not.
+}
diff --git a/src/Common/Drawing2D/LineJoin.cs b/src/Common/Drawing2D/LineJoin.cs
new file mode 100644
index 0000000..5f5c51e
--- /dev/null
+++ b/src/Common/Drawing2D/LineJoin.cs
@@ -0,0 +1,29 @@
+namespace GeneXus.Drawing.Drawing2D;
+
+///
+/// Specifies how to join consecutive line or curve segments in a figure (subpath) contained in a object.
+///
+public enum LineJoin
+{
+ ///
+ /// Specifies a mitered join. This produces a sharp corner or a clipped corner, depending on whether the
+ /// length of the miter exceeds the miter limit.
+ ///
+ Miter = 0,
+
+ ///
+ /// Specifies a beveled join. This produces a diagonal corner.
+ ///
+ Bevel = 1,
+
+ ///
+ /// pecifies a circular join. This produces a smooth, circular arc between the lines.
+ ///
+ Round = 2,
+
+ ///
+ /// Specifies a mitered join. This produces a sharp corner or a beveled corner, depending on whether the
+ /// length of the miter exceeds the miter limit.
+ ///
+ MiterClipped = 3
+}
diff --git a/src/Common/Drawing2D/LinearGradientBrush.cs b/src/Common/Drawing2D/LinearGradientBrush.cs
new file mode 100644
index 0000000..fcdd01b
--- /dev/null
+++ b/src/Common/Drawing2D/LinearGradientBrush.cs
@@ -0,0 +1,427 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using SkiaSharp;
+
+namespace GeneXus.Drawing.Drawing2D;
+
+public sealed class LinearGradientBrush : Brush
+{
+ private RectangleF m_rect;
+ private WrapMode m_mode;
+ private Matrix m_transform;
+ private Blend m_factors;
+ private ColorBlend m_colors;
+ private bool m_gamma;
+
+ private LinearGradientBrush(RectangleF rect, Color[] colors, WrapMode mode, Matrix transform)
+ : base(new SKPaint { })
+ {
+ m_rect = rect;
+ m_mode = mode;
+ m_transform = transform;
+
+ m_gamma = false;
+
+ m_factors = new();
+ Array.Copy(new[] { 1f }, m_factors.Factors, 1);
+ Array.Copy(new[] { 0f }, m_factors.Positions, 1);
+
+ var uniform = GetUniformArray(colors.Length);
+
+ m_colors = new(colors.Length);
+ Array.Copy(colors, m_colors.Colors, colors.Length);
+ Array.Copy(uniform, m_colors.Positions, colors.Length);
+
+ UpdateShader(() => { });
+ }
+
+ ///
+ /// Initializes a new instance of the class with the specified
+ /// points and colors.
+ ///
+ public LinearGradientBrush(PointF point1, PointF point2, Color color1, Color color2)
+ : this(GetRectangle(point1, point2), color1, color2, 0, false) { }
+
+ ///
+ /// Initializes a new instance of the class with the specified
+ /// points and colors.
+ ///
+ public LinearGradientBrush(Point point1, Point point2, Color color1, Color color2)
+ : this(new PointF(point1.m_point), new PointF(point2.m_point), color1, color2) { }
+
+ ///
+ /// Creates a new instance of the class based on
+ /// a , starting and ending colors, and orientation.
+ ///
+ public LinearGradientBrush(RectangleF rect, Color color1, Color color2, LinearGradientMode mode)
+ : this(rect, color1, color2, GetAngle(mode), false) { }
+
+ ///
+ /// Creates a new instance of the class based on
+ /// a , starting and ending colors, and orientation.
+ ///
+ public LinearGradientBrush(Rectangle rect, Color color1, Color color2, LinearGradientMode mode)
+ : this(new RectangleF(rect.m_rect), color1, color2, mode) { }
+
+ ///
+ /// Creates a new instance of the class based on
+ /// a , starting and ending colors, and an orientation angle.
+ ///
+ public LinearGradientBrush(RectangleF rect, Color color1, Color color2, float angle, bool isAngleScaleable = false)
+ : this(rect, new[] { color1, color2 }, WrapMode.Tile, GetTransform(rect, angle, isAngleScaleable)) { }
+
+ ///
+ /// Creates a new instance of the class based on
+ /// a , starting and ending colors, and an orientation angle.
+ ///
+ public LinearGradientBrush(Rectangle rect, Color color1, Color color2, float angle, bool isAngleScaleable = false)
+ : this(new RectangleF(rect.m_rect), color1, color2, angle, isAngleScaleable) { }
+
+
+ #region IClonable
+
+ ///
+ /// Creates an exact copy of this .
+ ///
+ public override object Clone()
+ => new LinearGradientBrush(Rectangle, LinearColors, WrapMode, Transform)
+ {
+ Blend = Blend,
+ InterpolationColors = InterpolationColors,
+ GammaCorrection = GammaCorrection
+ };
+
+ #endregion
+
+
+ #region Properties
+
+ ///
+ /// Gets or sets a that specifies positions and factors
+ /// that define a custom falloff for the gradient.
+ ///
+ public Blend Blend
+ {
+ get => m_factors;
+ set => UpdateShader(() => m_factors = value ?? throw new ArgumentNullException(nameof(value)));
+ }
+
+ ///
+ /// Gets or sets a value indicating whether gamma correction is enabled for
+ /// this .
+ ///
+ public bool GammaCorrection
+ {
+ get => m_gamma;
+ set => UpdateShader(() => m_gamma = value);
+ }
+
+ ///
+ /// Gets or sets a that defines a multicolor
+ /// linear gradient.
+ ///
+ public ColorBlend InterpolationColors
+ {
+ get => m_colors;
+ set => UpdateShader(() =>
+ {
+ var colors = value ?? throw new ArgumentNullException(nameof(value));
+ if (colors.Positions[0] != 0 )
+ throw new ArgumentException("first element must be equal to 0.", nameof(value));
+ if (colors.Positions[colors.Positions.Length - 1] != 1 && colors.Positions.Length > 1)
+ throw new ArgumentException("last element must be equal to 1.", nameof(value));
+ m_colors = colors;
+ });
+ }
+
+ ///
+ /// Gets or sets the starting and ending colors of the gradient.
+ ///
+ public Color[] LinearColors
+ {
+ get => m_colors.Colors;
+ set => UpdateShader(() => m_colors = new()
+ {
+ Colors = value,
+ Positions = GetUniformArray(value.Length)
+ });
+ }
+
+ ///
+ /// Gets a rectangular region that defines the starting and ending points of the gradient.
+ ///
+ public RectangleF Rectangle => m_rect;
+
+ ///
+ /// Gets or sets a copy of the object
+ /// that defines a local geometric transformation for the image associated
+ /// with this object.
+ ///
+ public Matrix Transform
+ {
+ get => m_transform;
+ set => UpdateShader(() => m_transform = value);
+ }
+
+ ///
+ /// Gets or sets a WrapMode enumeration that indicates the wrap mode for this LinearGradientBrush.
+ ///
+ public WrapMode WrapMode
+ {
+ get => m_mode;
+ set => UpdateShader(() =>
+ {
+ if (value is < WrapMode.Tile or > WrapMode.Clamp)
+ throw new InvalidEnumArgumentException(nameof(value), (int)value, typeof(WrapMode));
+ m_mode = value;
+ });
+ }
+
+ #endregion
+
+
+ #region Methods
+
+ ///
+ /// Multiplies the object that represents the local
+ /// geometric transformation of this object
+ /// by the specified object in the specified order.
+ ///
+ public void MultiplyTransform(Matrix matrix, MatrixOrder order = MatrixOrder.Prepend)
+ => UpdateShader(() => Transform.Multiply(matrix, order));
+
+ ///
+ /// Resets the Transform property of this object to identity.
+ ///
+ public void ResetTransform()
+ => UpdateShader(() => Transform.Reset());
+
+ ///
+ /// Rotates the local geometric transformation of this object
+ /// by the specified amount in the specified order.
+ ///
+ public void RotateTransform(float angle, MatrixOrder order = MatrixOrder.Prepend)
+ => UpdateShader(() => Transform.Rotate(angle, order));
+
+ ///
+ /// Scales the local geometric transformation of this object
+ /// by the specified amounts in the specified order.
+ ///
+ public void ScaleTransform(float sx, float sy, MatrixOrder order = MatrixOrder.Prepend)
+ => UpdateShader(() => Transform.Scale(sx, sy, order));
+
+ ///
+ /// Translates the local geometric transformation of this object
+ /// by the specified dimensions in the specified order.
+ ///
+ public void TranslateTransform(float dx, float dy, MatrixOrder order)
+ => UpdateShader(() => Transform.Translate(dx, dy, order));
+
+ ///
+ /// Creates a gradient falloff based on a bell-shaped curve.
+ ///
+ public void SetSigmaBellShape(float focus, float scale = 1.0f)
+ => UpdateShader(() => m_factors = GetSigmaBellShape(focus, scale));
+
+ ///
+ /// Creates a linear gradient with a center color and a linear falloff to a single color on both ends.
+ ///
+ public void SetBlendTriangularShape(float focus, float scale = 1.0f)
+ => UpdateShader(() => m_factors = GetBlendTriangularShape(focus, scale));
+
+ #endregion
+
+
+ #region Utilities
+
+ private static RectangleF GetRectangle(PointF point1, PointF point2)
+ => new(point1.X, point1.Y, point1.X + point2.X, point1.Y + point2.Y);
+
+ private static float GetAngle(LinearGradientMode mode) => mode switch
+ {
+ LinearGradientMode.Horizontal => 0,
+ LinearGradientMode.Vertical => 90,
+ LinearGradientMode.ForwardDiagonal => 45,
+ LinearGradientMode.BackwardDiagonal => 135,
+ _ => throw new ArgumentException($"{mode} mode is not supported.", nameof(mode))
+ };
+
+ private static float[] GetUniformArray(int length)
+ => length < 2
+ ? throw new ArgumentException("at least two items are required.", nameof(length))
+ : Enumerable.Range(0, length).Select(i => 1f * i / (length - 1)).ToArray();
+
+ static private Matrix GetTransform(RectangleF rect, float angle, bool scale)
+ {
+ float radians = angle % 360 * (float)(Math.PI / 180);
+
+ float cos = (float)Math.Cos(radians);
+ float sin = (float)Math.Sin(radians);
+
+ float absCos = Math.Abs(cos);
+ float absSin = Math.Abs(sin);
+
+ float wRatio = (absCos * rect.Width + absSin * rect.Height) / rect.Width;
+ float hRatio = (absSin * rect.Width + absCos * rect.Height) / rect.Height;
+
+ float transX = rect.X + rect.Width / 2;
+ float transY = rect.Y + rect.Height / 2;
+
+ var transform = new Matrix();
+ transform.Translate(transX, transY);
+ transform.Rotate(angle);
+ transform.Scale(wRatio, hRatio);
+ transform.Translate(-transX, -transY);
+
+ if (scale && absCos > 5e-4 && absSin > 5e-4)
+ {
+ var points = new PointF[3]
+ {
+ new(rect.Left, rect.Top),
+ new(rect.Right, rect.Top),
+ new(rect.Left, rect.Bottom),
+ };
+
+ transform.TransformPoints(points);
+
+ float ratio = rect.Width /rect.Height;
+ if (sin > 0 && cos > 0)
+ {
+ float slope = GetSlope(radians, ratio);
+ points[0].Y = (slope * (points[0].X - rect.Left)) + rect.Top;
+ points[1].X = ((points[1].Y - rect.Bottom) / slope) + rect.Right;
+ points[2].X = ((points[2].Y - rect.Top) / slope) + rect.Left;
+ }
+ else if (sin > 0 && cos < 0)
+ {
+ float slope = GetSlope(radians - 1 / 2f * Math.PI, ratio);
+ points[0].X = ((points[0].Y - rect.Bottom) / slope) + rect.Right;
+ points[1].Y = (slope * (points[1].X - rect.Right)) + rect.Bottom;
+ points[2].Y = (slope * (points[2].X - rect.Left)) + rect.Top;
+ }
+ else if (sin < 0 && cos < 0)
+ {
+ float slope = GetSlope(radians, ratio);
+ points[0].Y = (slope * (points[0].X - rect.Right)) + rect.Bottom;
+ points[1].X = ((points[1].Y - rect.Top) / slope) + rect.Left;
+ points[2].X = ((points[2].Y - rect.Bottom) / slope) + rect.Right;
+ }
+ else
+ {
+ float slope = GetSlope(radians - 3 / 2f * Math.PI, ratio);
+ points[0].X = ((points[0].Y - rect.Y) / slope) + rect.X;
+ points[1].Y = (slope * (points[1].X - rect.Left)) + rect.Top;
+ points[2].Y = (slope * (points[2].X - rect.Right)) + rect.Bottom;
+ }
+
+ float m11 = (points[1].X - points[0].X) / rect.Width;
+ float m12 = (points[1].Y - points[0].Y) / rect.Width;
+ float m21 = (points[2].X - points[0].X) / rect.Height;
+ float m22 = (points[2].Y - points[0].Y) / rect.Height;
+
+ transform = new Matrix(m11, m12, m21, m22, 0, 0);
+ transform.Translate(-rect.X, -rect.Y);
+ }
+
+ return transform;
+
+ static float GetSlope(double angleRadians, float aspectRatio)
+ => -1.0f / (aspectRatio * (float)Math.Tan(angleRadians));
+ }
+
+ private static Color ApplyGamma(Color color, float gamma)
+ => Color.FromArgb(
+ color.A,
+ (int)(Math.Pow(color.R / 255.0, gamma) * 255),
+ (int)(Math.Pow(color.G / 255.0, gamma) * 255),
+ (int)(Math.Pow(color.B / 255.0, gamma) * 255));
+
+ private void UpdateShader(Action action)
+ {
+ action();
+
+ var points = new PointF[] { new(m_rect.Left, 0), new(m_rect.Right, 0) };
+
+ using var transform = new Matrix(m_transform.MatrixElements);
+ switch (m_mode)
+ {
+ case WrapMode.TileFlipX:
+ transform.Scale(-1, 1);
+ break;
+ case WrapMode.TileFlipY:
+ transform.Scale(1, -1);
+ break;
+ case WrapMode.TileFlipXY:
+ transform.Scale(-1, -1);
+ break;
+ }
+ transform.TransformPoints(points);
+ transform.Reset();
+
+ var start = points[0].m_point;
+ var end = points[1].m_point;
+ var matrix = transform.m_matrix;
+ var gamma = m_gamma ? 2.2f : 1.0f;
+ var mode = m_mode == WrapMode.Clamp ? SKShaderTileMode.Decal : SKShaderTileMode.Repeat;
+
+ var blend = new Dictionary();
+ if (m_factors.Positions.Length > 1)
+ {
+ for (int index = 0; index < m_factors.Positions.Length; index++)
+ {
+ var pos = m_factors.Positions[index];
+ var fac = m_factors.Factors[index];
+ blend[pos] = fac; // blend factor
+ }
+ }
+ if (m_colors.Positions.Length > 1)
+ {
+ for (int index = 0; index < m_colors.Positions.Length; index++)
+ {
+ var pos = m_colors.Positions[index];
+ var col = m_colors.Colors[index];
+ blend[pos] = col; // specific color
+ }
+ }
+
+ var positions = blend.Keys.OrderBy(key => key).ToArray();
+ var colors = new SKColor[positions.Length];
+
+ var lastColor = m_colors.Colors[0];
+ for (int index = 0; index < positions.Length; index++)
+ {
+ var key = positions[index];
+ var value = blend[key];
+ if (value is Color currColor)
+ {
+ var color = ApplyGamma(currColor, gamma);
+ colors[index] = color.m_color;
+ lastColor = currColor;
+ continue;
+ }
+ if (value is float factor)
+ {
+ var nextColor = m_colors.Colors[m_colors.Colors.Length - 1];
+ for (int i = index + 1; i < positions.Length; i++)
+ {
+ key = positions[i];
+ value = blend[key];
+ if (value is Color foundColor)
+ {
+ nextColor = foundColor;
+ break;
+ }
+ }
+ var color = Color.Blend(lastColor, nextColor, factor);
+ colors[index] = color.m_color;
+ continue;
+ }
+ }
+
+ m_paint.Shader = SKShader.CreateLinearGradient(start, end, colors, positions, mode, matrix);
+ }
+
+ #endregion
+}
diff --git a/src/Common/Drawing2D/LinearGradientMode.cs b/src/Common/Drawing2D/LinearGradientMode.cs
new file mode 100644
index 0000000..8907963
--- /dev/null
+++ b/src/Common/Drawing2D/LinearGradientMode.cs
@@ -0,0 +1,27 @@
+namespace GeneXus.Drawing.Drawing2D;
+
+///
+/// Specifies the direction of a linear gradient.
+///
+public enum LinearGradientMode
+{
+ ///
+ /// Specifies a gradient from left to right.
+ ///
+ Horizontal = 0,
+
+ ///
+ /// Specifies a gradient from top to bottom.
+ ///
+ Vertical = 1,
+
+ ///
+ /// Specifies a gradient from upper left to lower right.
+ ///
+ ForwardDiagonal = 2,
+
+ ///
+ /// Specifies a gradient from upper right to lower left.
+ ///
+ BackwardDiagonal = 3
+}
diff --git a/src/Common/Drawing2D/Matrix.cs b/src/Common/Drawing2D/Matrix.cs
new file mode 100644
index 0000000..606f453
--- /dev/null
+++ b/src/Common/Drawing2D/Matrix.cs
@@ -0,0 +1,378 @@
+
+using System;
+using System.Numerics;
+using SkiaSharp;
+
+namespace GeneXus.Drawing.Drawing2D;
+
+public sealed class Matrix : ICloneable, IDisposable
+{
+ internal SKMatrix m_matrix;
+
+ internal Matrix(SKMatrix matrix)
+ {
+ m_matrix = matrix;
+ }
+
+ ///
+ /// Initializes a new instance of the class as the identity matrix.
+ ///
+ public Matrix()
+ : this(SKMatrix.CreateIdentity()) { }
+
+ ///
+ /// Initializes a new instance of the class with the specified elements.
+ ///
+ public Matrix(float m11, float m12, float m21, float m22, float dx, float dy)
+ : this(CreateElementsMatrix(m11, m12, m21, m22, dx, dy)) { }
+
+ ///
+ /// Constructs a utilizing the specified matrix.
+ ///
+ ///
+ public Matrix(Matrix3x2 matrix)
+ : this(matrix.M11, matrix.M12, matrix.M21, matrix.M22, matrix.M31, matrix.M32) { }
+
+ ///
+ /// Initializes a new instance of the class to the geometric transform
+ /// defined by the specified and array of .
+ ///
+ public Matrix(RectangleF rect, PointF[] plgpts)
+ : this(CreateTransformMatrix(
+ new PointF[]
+ {
+ new(rect.Left, rect.Top),
+ new(rect.Right, rect.Top),
+ new(rect.Left, rect.Bottom)
+ },
+ plgpts))
+ { }
+
+ ///
+ /// Initializes a new instance of the class to the geometric transform
+ /// defined by the specified and array of .
+ ///
+ public Matrix(Rectangle rect, Point[] plgpts)
+ : this(new RectangleF(rect.m_rect), Array.ConvertAll(plgpts, point => new PointF(point.m_point))) { }
+
+ ///
+ /// Cleans up resources for this .
+ ///
+ ~Matrix() => Dispose(false);
+
+
+ #region IDisposble
+
+ ///
+ /// Cleans up resources for this .
+ ///
+ public void Dispose()
+ {
+ GC.SuppressFinalize(this);
+ Dispose(true);
+ }
+
+ private void Dispose(bool disposing) { }
+
+ #endregion
+
+
+ #region IClonable
+
+ ///
+ /// Creates an exact copy of this .
+ ///
+ public object Clone()
+ => new Matrix(m_matrix);
+
+ #endregion
+
+
+ #region IEqualitable
+
+ ///
+ /// Tests whether the specified object is a and is identical to this .
+ ///
+ public override bool Equals(object obj)
+ => obj is Matrix matrix && m_matrix.Equals(matrix.m_matrix);
+
+ ///
+ /// Get the has code of this .
+ ///
+ public override int GetHashCode()
+ => m_matrix.GetHashCode();
+
+ #endregion
+
+
+ #region Operators
+
+ ///
+ /// Creates a with specified .
+ ///
+ public static explicit operator SKMatrix(Matrix matrix) => matrix.m_matrix;
+
+ ///
+ /// Tests whether two objects are identical.
+ ///
+ public static bool operator ==(Matrix left, Matrix right) => left.m_matrix == right.m_matrix;
+
+ ///
+ /// Tests whether two objects are different.
+ ///
+ public static bool operator !=(Matrix left, Matrix right) => left.m_matrix != right.m_matrix;
+
+ #endregion
+
+
+ #region Properties
+
+ ///
+ /// Gets an array of floating-point values that represents the elements of this .
+ ///
+ public float[] Elements => new float[]
+ {
+ m_matrix.ScaleX, m_matrix.SkewX,
+ m_matrix.SkewY, m_matrix.ScaleY,
+ m_matrix.TransX, m_matrix.TransY
+ };
+
+ ///
+ /// Gets a value indicating whether this is the identity matrix.
+ ///
+ public bool IsIdentity => m_matrix.IsIdentity;
+
+ ///
+ /// Gets a value indicating whether this is invertible.
+ ///
+ public bool IsInvertible => m_matrix.IsInvertible;
+
+ ///
+ /// Gets or sets the elements for the matrix.
+ ///
+ public Matrix3x2 MatrixElements
+ {
+ get => new(
+ m_matrix.ScaleX,
+ m_matrix.SkewX,
+ m_matrix.SkewY,
+ m_matrix.ScaleY,
+ m_matrix.TransX,
+ m_matrix.TransY
+ );
+ set
+ {
+ m_matrix.ScaleX = value.M11;
+ m_matrix.SkewX = value.M12;
+ m_matrix.SkewY = value.M21;
+ m_matrix.ScaleY = value.M22;
+ m_matrix.TransX = value.M31;
+ m_matrix.TransY = value.M32;
+ m_matrix.Persp0 = 0;
+ m_matrix.Persp1 = 0;
+ m_matrix.Persp2 = 1;
+ }
+ }
+
+ ///
+ /// Gets the x translation value (the dx value, or the element in the third row and first column)
+ /// of this .
+ ///
+ public float OffsetX => m_matrix.TransX;
+
+ ///
+ /// Gets the y translation value (the dy value, or the element in the third row and second column)
+ /// of this .
+ ///
+ public float OffsetY => m_matrix.TransY;
+
+ #endregion
+
+
+ #region Methods
+
+ ///
+ /// Inverts this , if it is invertible.
+ ///
+ public void Invert()
+ => m_matrix = Invert(m_matrix);
+
+ ///
+ /// Multiplies this by the matrix specified in the parameter,
+ /// and in the order specified in the parameter.
+ ///
+ private void Multiply(SKMatrix matrix, MatrixOrder order = MatrixOrder.Prepend)
+ => m_matrix = order == MatrixOrder.Prepend ? Multiply(matrix, m_matrix) : Multiply(m_matrix, matrix);
+
+ ///
+ /// Multiplies this by the matrix specified in the parameter,
+ /// and in the order specified in the parameter.
+ ///
+ public void Multiply(Matrix matrix, MatrixOrder order = MatrixOrder.Prepend)
+ => Multiply(matrix.m_matrix, order);
+
+ ///
+ /// Resets this to have the elements of the identity matrix.
+ ///
+ public void Reset()
+ => m_matrix = SKMatrix.CreateIdentity();
+
+ ///
+ /// Applies a clockwise rotation of the specified angle about the origin to this .
+ ///
+ public void Rotate(float angle, MatrixOrder order = MatrixOrder.Prepend)
+ => Multiply(SKMatrix.CreateRotationDegrees(-angle), order);
+
+ ///
+ /// Applies a clockwise rotation about the specified point to this .
+ ///
+ public void RotateAt(float angle, PointF point, MatrixOrder order = MatrixOrder.Prepend)
+ {
+ Translate(point.X, point.Y, order);
+ Rotate(angle, order);
+ Translate(-point.X, -point.Y, order);
+ }
+
+ ///
+ /// Applies a clockwise rotation about the specified point to this .
+ ///
+ public void RotateAt(float angle, Point point, MatrixOrder order = MatrixOrder.Prepend)
+ => RotateAt(angle, new PointF(point.m_point), order);
+
+ ///
+ /// Applies the specified scale vector to this by prepending the scale vector.
+ ///
+ public void Scale(float scaleX, float scaleY, MatrixOrder order = MatrixOrder.Prepend)
+ => Multiply(SKMatrix.CreateScale(scaleX, scaleY), order);
+
+ ///
+ /// Applies the specified shear vector to this by prepending the shear vector.
+ ///
+ public void Shear(float shearX, float shearY, MatrixOrder order = MatrixOrder.Prepend)
+ => Multiply(SKMatrix.CreateSkew(shearY, shearX), order);
+
+ ///
+ /// Applies the specified translation vector to this by prepending the translation vector.
+ ///
+ public void Translate(float offsetX, float offsetY, MatrixOrder order = MatrixOrder.Prepend)
+ => Multiply(SKMatrix.CreateTranslation(offsetX, offsetY), order);
+
+ ///
+ /// Applies the geometric transform represented by this to a specified
+ /// array of .
+ ///
+ public void TransformPoints(PointF[] points)
+ => TransformPoints(points, Transpose(m_matrix), p => new(p), p => p.m_point);
+
+ ///
+ /// Applies the geometric transform represented by this to a specified
+ /// array of .
+ ///
+ public void TransformPoints(Point[] points)
+ => TransformPoints(points, Transpose(m_matrix), p => new(p), p => p.m_point);
+
+ ///
+ /// Applies only the scale and rotate components of this to the specified
+ /// array of .
+ ///
+ public void TransformVectors(Point[] points)
+ => TransformVectors(points, p => new(p), p => p.m_point);
+
+ ///
+ /// Multiplies each vector in an array by this . The translation elements of
+ /// this matrix (third row) are ignored.
+ ///
+ public void VectorTransformPoints(Point[] points)
+ => TransformVectors(points);
+
+ #endregion
+
+
+ #region Utilities
+
+ private const float DEG_OFFSET = -90;
+
+ /*
+ * NOTE: SkiaSharp and System.Drawing have a different
+ * representation for transformation matrices
+ *
+ * SkiaSharp: System.Drawing:
+ * ┌ ┐ ┌ ┐
+ * │ ScaleX SkewX TransX │ │ ScaleX SkewX PerspX │
+ * │ SkewY ScaleY TransY │ │ SkewY ScaleY PerspY │
+ * │ PerspX PerspY PerspZ │ │ TransX TransY PerspZ │
+ * └ ┘ └ ┘
+ */
+
+ private static SKMatrix CreateElementsMatrix(float m11, float m12, float m21, float m22, float m31, float m32)
+ => new(m11, m12, m31, m21, m22, m32, 0, 0, 1);
+
+ private static SKMatrix CreateTransformMatrix(PointF[] src, PointF[] dst)
+ {
+ if (src.Length < 3) throw new ArgumentException("must contain 3 points.", nameof(src));
+ if (dst.Length < 3) throw new ArgumentException("must contain 3 points.", nameof(dst));
+
+ float den = (src[0].X - src[1].X) * (src[0].Y - src[2].Y) - (src[0].X - src[2].X) * (src[0].Y - src[1].Y);
+ if (den == 0) throw new InvalidOperationException("cannot create a valid transformation matrix for the given points.");
+
+ float m11 = (dst[0].X * (src[1].Y - src[2].Y) + dst[1].X * (src[2].Y - src[0].Y) + dst[2].X * (src[0].Y - src[1].Y)) / den;
+ float m12 = (dst[0].Y * (src[1].Y - src[2].Y) + dst[1].Y * (src[2].Y - src[0].Y) + dst[2].Y * (src[0].Y - src[1].Y)) / den;
+ float dx = dst[0].X - m11 * src[0].X - m12 * src[0].Y;
+
+ float m21 = (dst[0].X * (src[2].X - src[1].X) + dst[1].X * (src[0].X - src[2].X) + dst[2].X * (src[1].X - src[0].X)) / den;
+ float m22 = (dst[0].Y * (src[2].X - src[1].X) + dst[1].Y * (src[0].X - src[2].X) + dst[2].Y * (src[1].X - src[0].X)) / den;
+ float dy = dst[0].Y - m21 * src[0].X - m22 * src[0].Y;
+
+ return CreateElementsMatrix(m11, m12, m21, m22, dx, dy);
+ }
+
+ private static SKMatrix Multiply(SKMatrix a, SKMatrix b)
+ => SwapTrans(SKMatrix.Concat(SwapTrans(a), SwapTrans(b)));
+
+ private static SKMatrix Invert(SKMatrix matrix)
+ => SwapTrans(matrix).TryInvert(out var invert)
+ ? SwapTrans(invert)
+ : throw new InvalidOperationException("matrix inversion failed.");
+
+ private static SKMatrix SwapTrans(SKMatrix matrix)
+ => new( // swaps (TransX, TransY) with (Persp0, Persp1)
+ matrix.ScaleX, matrix.SkewX, matrix.Persp0,
+ matrix.SkewY, matrix.ScaleY, matrix.Persp1,
+ matrix.TransX, matrix.TransY, matrix.Persp2);
+
+ private static SKMatrix Transpose(SKMatrix matrix)
+ => SwapTrans(new( // transposes the matrix
+ matrix.ScaleX, matrix.SkewY, matrix.Persp0,
+ matrix.SkewX, matrix.ScaleY, matrix.Persp1,
+ matrix.TransX, matrix.TransY, matrix.Persp2));
+
+ private static void TransformPoints(T[] points, SKMatrix matrix, Func newPoint, Func getPoint)
+ {
+ for (int i = 0; i < points.Length; i++)
+ {
+ var srcPoint = getPoint(points[i]);
+ var dstPoint = matrix.MapPoint(srcPoint);
+ points[i] = newPoint(dstPoint);
+ }
+ }
+
+ private void TransformVectors(T[] points, Func newPoint, Func getPoint)
+ {
+ var transformMatrix = new SKMatrix
+ {
+ ScaleX = m_matrix.ScaleX,
+ SkewX = m_matrix.SkewX,
+ TransX = 0,
+ SkewY = m_matrix.SkewY,
+ ScaleY = m_matrix.ScaleY,
+ TransY = 0,
+ Persp0 = m_matrix.Persp0,
+ Persp1 = m_matrix.Persp1,
+ Persp2 = 1
+ };
+ TransformPoints(points, Transpose(transformMatrix), newPoint, getPoint);
+ }
+
+ #endregion
+}
+
\ No newline at end of file
diff --git a/src/Common/Drawing2D/MatrixOrder.cs b/src/Common/Drawing2D/MatrixOrder.cs
new file mode 100644
index 0000000..6d511b5
--- /dev/null
+++ b/src/Common/Drawing2D/MatrixOrder.cs
@@ -0,0 +1,17 @@
+namespace GeneXus.Drawing.Drawing2D;
+
+///
+/// Specifies the order for matrix transform operations.
+///
+public enum MatrixOrder
+{
+ ///
+ /// The new operation is applied after the old operation.
+ ///
+ Prepend = 0,
+
+ ///
+ /// The new operation is applied before the old operation.
+ ///
+ Append = 1
+}
diff --git a/src/Common/Drawing2D/PathData.cs b/src/Common/Drawing2D/PathData.cs
new file mode 100644
index 0000000..a0c0aea
--- /dev/null
+++ b/src/Common/Drawing2D/PathData.cs
@@ -0,0 +1,25 @@
+namespace GeneXus.Drawing.Drawing2D;
+
+public sealed class PathData
+{
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public PathData() { }
+
+
+ #region Properties
+
+ ///
+ /// Gets or sets an array of structures that represents the points through which the path is constructed.
+ ///
+ public PointF[] Points { get; set; }
+
+ ///
+ /// Gets or sets the types of the corresponding points in the path.
+ ///
+ public byte[] Types { get; set; }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/src/Common/Drawing2D/PathGradientBrush.cs b/src/Common/Drawing2D/PathGradientBrush.cs
new file mode 100644
index 0000000..a1e82f8
--- /dev/null
+++ b/src/Common/Drawing2D/PathGradientBrush.cs
@@ -0,0 +1,549 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Numerics;
+using SkiaSharp;
+
+namespace GeneXus.Drawing.Drawing2D;
+
+public sealed class PathGradientBrush : Brush
+{
+ private GraphicsPath m_path;
+ private WrapMode m_mode;
+ private Matrix m_transform;
+ private Blend m_factors;
+ private ColorBlend m_colors;
+ private PointF m_center, m_focus;
+ private Color? m_color;
+ private Color[] m_surround;
+
+ private PathGradientBrush(GraphicsPath path, WrapMode mode, Matrix transform)
+ : base(new SKPaint { })
+ {
+ var points = path.PathPoints;
+ if (points.First() == points.Last())
+ points = points.Take(points.Length - 1).ToArray();
+
+ m_path = path;
+ m_mode = mode;
+ m_transform = transform;
+
+ m_factors = new();
+ Array.Copy(new[] { 1f }, m_factors.Factors, 1);
+ Array.Copy(new[] { 0f }, m_factors.Positions, 1);
+
+ m_colors = new();
+ Array.Copy(new[] { Color.Empty }, m_colors.Colors, 1);
+ Array.Copy(new[] { 0f }, m_colors.Positions, 1);
+
+ m_center = new(
+ points.Average(pt => pt.X),
+ points.Average(pt => pt.Y));
+
+ m_focus = new(0, 0);
+ m_surround = new[] { Color.White };
+
+ UpdateShader(() => { });
+ }
+
+ ///
+ /// Initializes a new instance of the class with the specified path.
+ ///
+ ///
+ public PathGradientBrush(GraphicsPath path)
+ : this(path, WrapMode.Clamp, new Matrix()) { }
+
+ ///
+ /// Initializes a new instance of the class with the specified points.
+ ///
+ public PathGradientBrush(params PointF[] points)
+ : this(points, WrapMode.Clamp) { }
+
+ ///
+ /// Initializes a new instance of the class with the specified points and wrap mode.
+ ///
+ public PathGradientBrush(PointF[] points, WrapMode mode)
+ : this(CreatePath(points), mode, new Matrix()) { }
+
+ ///
+ /// Initializes a new instance of the class with the specified points.
+ ///
+ public PathGradientBrush(params Point[] points)
+ : this(points, WrapMode.Clamp) { }
+
+ ///
+ /// Initializes a new instance of the class with the specified points and wrap mode.
+ ///
+ public PathGradientBrush(Point[] points, WrapMode mode)
+ : this(Array.ConvertAll(points, point => new PointF(point.m_point)), mode) { }
+
+
+ #region IClonable
+
+ ///
+ /// Creates an exact copy of this .
+ ///
+ public override object Clone()
+ {
+ var path = new GraphicsPath(m_path.PathPoints, m_path.PathTypes, m_path.FillMode);
+ var transform = new Matrix(m_transform.MatrixElements);
+ return new PathGradientBrush(path, WrapMode, transform)
+ {
+ Blend = Blend,
+ CenterColor = CenterColor,
+ CenterPoint = CenterPoint,
+ FocusScales = FocusScales,
+ InterpolationColors = InterpolationColors,
+ SurroundColors = SurroundColors,
+ };
+ }
+
+ #endregion
+
+
+ #region Properties
+
+ ///
+ /// Gets or sets a that specifies positions and factors that define a
+ /// custom falloff for the gradient.
+ ///
+ public Blend Blend
+ {
+ get => m_factors;
+ set => UpdateShader(() => m_factors = value ?? throw new ArgumentNullException(nameof(value)));
+ }
+
+ ///
+ /// Gets or sets the at the center of the path gradient.
+ ///
+ public Color CenterColor
+ {
+ get => m_color ?? (m_surround.Length > 1 ? Color.Black : Color.White);
+ set => UpdateShader(() => m_color = value);
+ }
+
+ ///
+ /// Gets or sets the center of the path gradient.
+ ///
+ public PointF CenterPoint
+ {
+ get => m_center;
+ set => UpdateShader(() => m_center = value);
+ }
+
+ ///
+ /// Gets or sets the focus for the gradient falloff.
+ ///
+ public PointF FocusScales
+ {
+ get => m_focus;
+ set => UpdateShader(() => m_focus = value);
+ }
+
+ ///
+ /// Gets or sets a that defines a multicolor linear gradient.
+ ///
+ public ColorBlend InterpolationColors
+ {
+ get => m_colors;
+ set => UpdateShader(() =>
+ {
+ var interpolation = value ?? throw new ArgumentNullException(nameof(value));
+ if (interpolation.Positions[0] != 0)
+ throw new ArgumentException("first element must be equal to 0.", nameof(value));
+ if (interpolation.Positions[interpolation.Positions.Length - 1] != 1 && interpolation.Positions.Length > 1)
+ throw new ArgumentException("last element must be equal to 1.", nameof(value));
+ m_colors = interpolation;
+ });
+ }
+
+ ///
+ /// Gets a bounding for this gradient.
+ ///
+ public RectangleF Rectangle => m_path.GetBounds();
+
+ ///
+ /// Gets or sets an array of structures that correspond to the
+ /// points in the path this gradient fills.
+ ///
+ public Color[] SurroundColors
+ {
+ get => m_surround;
+ set => UpdateShader(() =>
+ {
+ if (value.Length > m_path.PointCount)
+ throw new ArgumentException("parameter is not valid.", nameof(value));
+ m_surround = value;
+ });
+ }
+
+ ///
+ /// Gets or sets a copy of the that defines a local geometric
+ /// transform for this gradient.
+ ///
+ public Matrix Transform
+ {
+ get => m_transform;
+ set => UpdateShader(() => m_transform = value);
+ }
+
+ ///
+ /// Gets or sets a that indicates the wrap mode for this gradient.
+ ///
+ public WrapMode WrapMode
+ {
+ get => m_mode;
+ set => UpdateShader(() =>
+ {
+ if (value is < WrapMode.Tile or > WrapMode.Clamp)
+ throw new InvalidEnumArgumentException(nameof(value), (int)value, typeof(WrapMode));
+ m_mode = value;
+ });
+ }
+
+ #endregion
+
+
+ #region Methods
+
+ ///
+ /// Multiplies the object that represents the local
+ /// geometric transformation of this object
+ /// by the specified object in the specified order.
+ ///
+ public void MultiplyTransform(Matrix matrix, MatrixOrder order = MatrixOrder.Prepend)
+ => UpdateShader(() => Transform.Multiply(matrix, order));
+
+ ///
+ /// Resets the Transform property of this object to identity.
+ ///
+ public void ResetTransform()
+ => UpdateShader(() => Transform.Reset());
+
+ ///
+ /// Rotates the local geometric transformation of this object
+ /// by the specified amount in the specified order.
+ ///
+ public void RotateTransform(float angle, MatrixOrder order = MatrixOrder.Prepend)
+ => UpdateShader(() => Transform.Rotate(angle, order));
+
+ ///
+ /// Scales the local geometric transformation of this object
+ /// by the specified amounts in the specified order.
+ ///
+ public void ScaleTransform(float sx, float sy, MatrixOrder order = MatrixOrder.Prepend)
+ => UpdateShader(() => Transform.Scale(sx, sy, order));
+
+ ///
+ /// Creates a gradient with a center color and a linear falloff to each surrounding color.
+ ///
+ public void SetBlendTriangularShape(float focus, float scale = 1.0f)
+ => UpdateShader(() => m_factors = GetBlendTriangularShape(focus, scale));
+
+ ///
+ /// Creates a gradient brush that changes color starting from the center of the path outward
+ /// to the path's boundary. The transition from one color to another is based on a bell-shaped curve.
+ ///
+ public void SetSigmaBellShape(float focus, float scale = 1.0f)
+ => UpdateShader(() => m_factors = GetSigmaBellShape(focus, scale));
+
+ ///
+ /// Translates the local geometric transformation of this object
+ /// by the specified dimensions in the specified order.
+ ///
+ public void TranslateTransform(float dx, float dy, MatrixOrder order = MatrixOrder.Prepend)
+ => UpdateShader(() => Transform.Translate(dx, dy, order));
+
+ #endregion
+
+
+ #region Utilities
+
+ private static GraphicsPath CreatePath(PointF[] points)
+ {
+ var types = new byte[points.Length];
+ types[0] = (byte)PathPointType.Start;
+ for (int i = 1; i < types.Length; i++)
+ types[i] = (byte)PathPointType.Line;
+ types[types.Length - 1] |= (byte)PathPointType.CloseSubpath;
+ return new GraphicsPath(points, types);
+ }
+
+ private static Vector2? Intersect(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3)
+ {
+ Vector2 d1 = p1 - p0; // line p0-p1
+ Vector2 d2 = p3 - p2; // line p2-p3
+
+ float det = d1.X * d2.Y - d1.Y * d2.X;
+ if (Math.Abs(det) < 1e-6) // check if lines are parallel
+ return null;
+
+ // point intersection of p0-p1 and p2-p3
+ float t = ((p2.X - p0.X) * d2.Y - (p2.Y - p0.Y) * d2.X) / det;
+ return p0 + t * d1;
+ }
+
+ private static bool Triangulated(Vector2 pt, Vector2 p0, Vector2 p1, Vector2 p2)
+ {
+ bool EdgeCheck(Vector2 a, Vector2 b)
+ => (a.Y <= pt.Y && pt.Y < b.Y || b.Y <= pt.Y && pt.Y < a.Y)
+ && (pt.X + 1e-5 < (b.X - a.X) * (pt.Y - a.Y) / (b.Y - a.Y) + a.X);
+
+ // check if point is in the triangle by checking the edges
+ return EdgeCheck(p0, p2) ^ EdgeCheck(p1, p0) ^ EdgeCheck(p2, p1);
+ }
+
+ private static Vector2 Project(Vector2 pt, Vector2 p0, Vector2 p1)
+ {
+ Vector2 e0 = p1 - p0;
+ Vector2 e1 = pt - p0;
+
+ // determine the numerator and denominator for scalar factor
+ float num = Vector2.Dot(e0, e1);
+ float den = Vector2.Dot(e0, e0) + float.Epsilon;
+ float t = Math.Min(1, Math.Max(0, num / den));
+
+ // point projection in the line p0-p1
+ return p0 + t * e0;
+ }
+
+ private static Vector2 Bezier(Vector2 p0, Vector2 p1, Vector2 p2, float percent)
+ {
+ Vector2 c0 = Vector2.Lerp(p0, p1, percent);
+ Vector2 c1 = Vector2.Lerp(p1, p2, percent);
+
+ // point in the bezier curve p0-p1-p2 at percent
+ return Vector2.Lerp(c0, c1, percent);
+ }
+
+ private Color ComputeColor(int x, int y, Vector2 center, Vector2[] outer, Vector2[] inner, PathPointType[] types, Color[] colors, float[] positions)
+ {
+ var color = Color.Transparent;
+ if (m_path.IsVisible(x, y))
+ {
+ var target = new Vector2(x, y);
+ if (m_surround.Length > 1)
+ {
+ // determine the weights of this point to the corners of the path
+ var weight = new float[outer.Length];
+ for (int i = 0; i < outer.Length; i++)
+ {
+ var p0 = outer[(i + 0) % outer.Length];
+ var p1 = outer[(i + 1) % outer.Length];
+
+ var ep = Project(target, p0, p1);
+
+ int j = (i + outer.Length - 1) % outer.Length;
+ weight[j] = Vector2.Distance(target, ep);
+ }
+
+ float total = weight.Sum(); // for normalizing in 0..1
+
+ // determine the blended color for the given point by weights
+ color = colors[colors.Length - 1];
+ for (int i = 0; i < weight.Length; i++)
+ {
+ float amount = weight[i] / total;
+ int j = Math.Min(i, colors.Length - 1); // NOTE: there could be less colors than vertices
+ color = Color.Blend(color, colors[j], amount);
+ }
+
+ // change the colors and positions according to surrounded colors
+ colors = new[] { color, colors[colors.Length - 1] };
+ positions = new[] { 0f, 1f };
+ }
+
+ // determine the distance of this point to the edge of the path
+ float dist = float.MaxValue, dmax = 0f;
+ for (int i = 0; i < outer.Length; i++)
+ {
+ var p0 = outer[(i + 0) % outer.Length];
+ var p1 = outer[(i + 1) % outer.Length];
+ var p2 = outer[(i + 2) % outer.Length];
+
+ var c0 = inner[(i + 0) % inner.Length];
+ var c1 = inner[(i + 1) % inner.Length];
+ var c2 = inner[(i + 2) % inner.Length];
+
+ var type = types[i % types.Length];
+ switch(type)
+ {
+ case PathPointType.Line:
+ if (Triangulated(target, p0, p1, center))
+ {
+ var ep = Intersect(target, center, p0, p1) ?? throw new Exception("line defined by target and center is parallel to shape edge");
+ var ip = Intersect(target, center, c0, c1) ?? center;
+
+ ep = Project(ep, p0, p1);
+ ip = Project(ip, c0, c1);
+
+ dist = Vector2.Distance(target, ep);
+ dmax = Vector2.Distance(ip, ep);
+
+ i = outer.Length; // break the loop
+ }
+ break;
+
+ case PathPointType.Bezier:
+ for (float t = 0; t <= 1; t += 0.1f)
+ {
+ var bp = Bezier(p0, p1, p2, t);
+ var cp = Bezier(c0, c1, c2, t);
+
+ var distCp = Vector2.Distance(target, bp);
+ var dmaxCp = Vector2.Distance(cp, bp);
+
+ if (distCp < dist)
+ {
+ dist = distCp;
+ dmax = dmaxCp;
+ }
+ }
+ i++;
+ break;
+
+ default:
+ throw new ArgumentException($"unknown type 0x{type:X2} at index {i}", nameof(types));
+ }
+ }
+
+ // normalized in 0..1
+ dist /= dmax;
+
+ // determine the blended color for the given point by positions
+ color = colors[colors.Length - 1];
+ for (int i = 0; i < positions.Length - 1; i++)
+ {
+ if (dist >= positions[i] && dist < positions[i + 1])
+ {
+ float amount = (dist - positions[i]) / (positions[i + 1] - positions[i]);
+ color = Color.Blend(colors[i], colors[i + 1], amount);
+ break;
+ }
+ }
+ }
+ return color;
+ }
+
+ private void UpdateShader(Action action)
+ {
+ action();
+
+ using var transform = new Matrix(m_transform.MatrixElements);
+ switch (m_mode)
+ {
+ case WrapMode.TileFlipX:
+ transform.Scale(-1, 1);
+ break;
+ case WrapMode.TileFlipY:
+ transform.Scale(1, -1);
+ break;
+ case WrapMode.TileFlipXY:
+ transform.Scale(-1, -1);
+ break;
+ }
+
+ var bounds = m_path.GetBounds();
+ var center = m_center.ToVector2();
+
+ var outer = m_path.PathPoints
+ .Select(point => point.ToVector2())
+ .ToArray();
+ var types = m_path.PathTypes
+ .Select(type => (PathPointType)(type & (byte)PathPointType.PathTypeMask))
+ .Skip(1) // skip PathPointType.Start
+ .ToArray();
+
+ var focus = Matrix3x2.CreateScale(m_focus.X, m_focus.Y);
+ var inner = outer
+ .Select(point => Vector2.Transform(point - center, focus) + center)
+ .ToArray();
+
+ var blend = new Dictionary() { [0] = m_surround[0], [1] = m_color };
+ if (m_surround.Length > 1)
+ {
+ blend[1] ??= Color.Black;
+ for (int index = 0; index < m_surround.Length; index++)
+ {
+ var pos = 1f * index / m_surround.Length;
+ var col = m_surround[index];
+ blend[pos] = col; // corner color
+ }
+ }
+ else
+ {
+ blend[1] ??= Color.White;
+ if (m_factors.Positions.Length > 1)
+ {
+ for (int index = 0; index < m_factors.Positions.Length; index++)
+ {
+ var pos = m_factors.Positions[index];
+ var fac = m_factors.Factors[index];
+ blend[pos] = fac; // edge factor
+ }
+ }
+ if (m_colors.Positions.Length > 1)
+ {
+ for (int index = 0; index < m_colors.Positions.Length; index++)
+ {
+ var pos = m_colors.Positions[index];
+ var col = m_colors.Colors[index];
+ blend[pos] = col; // edge color
+ }
+ }
+ }
+
+ var positions = blend.Keys.OrderBy(key => key).ToArray();
+ var colors = new Color[positions.Length];
+
+ var lastColor = m_surround[0];
+ for (int index = 0; index < positions.Length; index++)
+ {
+ var key = positions[index];
+ var value = blend[key];
+ if (value is Color currColor)
+ {
+ colors[index] = currColor;
+ lastColor = currColor;
+ continue;
+ }
+ if (value is float factor)
+ {
+ var nextColor = m_color ?? Color.White;
+ for (int i = index + 1; i < positions.Length; i++)
+ {
+ key = positions[i];
+ value = blend[key];
+ if (value is Color foundColor)
+ {
+ nextColor = foundColor;
+ break;
+ }
+ }
+ var color = Color.Blend(lastColor, nextColor, factor);
+ colors[index] = color;
+ continue;
+ }
+ }
+
+ // NOTE: Skia does not offers path gradient shader, that's why we use a bitmap
+ using var bitmap = new Bitmap(bounds.Width + bounds.Left, bounds.Height + bounds.Top);
+ for (int x = 0; x < bitmap.Width; x++)
+ {
+ for (int y = 0; y < bitmap.Height; y++)
+ {
+ var color = ComputeColor(x, y, center, outer, inner, types, colors, positions);
+ bitmap.SetPixel(x, y, color);
+ }
+ }
+
+ var source = bitmap.m_bitmap;
+ var matrix = transform.m_matrix;
+ var mode = m_mode == WrapMode.Clamp ? SKShaderTileMode.Decal : SKShaderTileMode.Repeat;
+
+ m_paint.Shader = SKShader.CreateBitmap(source, mode, mode, matrix);
+ }
+
+ #endregion
+}
diff --git a/src/Common/Drawing2D/PathPointType.cs b/src/Common/Drawing2D/PathPointType.cs
new file mode 100644
index 0000000..de55c68
--- /dev/null
+++ b/src/Common/Drawing2D/PathPointType.cs
@@ -0,0 +1,47 @@
+namespace GeneXus.Drawing.Drawing2D;
+
+///
+/// Specifies the type of point in a object.
+///
+public enum PathPointType : byte
+{
+ ///
+ /// Indicates that the point is the start of a figure.
+ ///
+ Start = 0x00,
+
+ ///
+ /// Indicates that the point is an endpoint of a line.
+ ///
+ Line = 0x01,
+
+ ///
+ /// Indicates that the point is an endpoint or a control point of a cubic Bézier spline.
+ ///
+ Bezier = 0x03,
+
+ ///
+ /// Masks all bits except for the three low-order bits, which indicate the point type.
+ ///
+ PathTypeMask = 0x07,
+
+ ///
+ /// Not used.
+ ///
+ DashMode = 0x10,
+
+ ///
+ /// Specifies that the point is a marker.
+ ///
+ PathMarker = 0x20,
+
+ ///
+ /// Specifies that the point is the last point in a closed subpath (figure).
+ ///
+ CloseSubpath = 0x80,
+
+ ///
+ /// A cubic Bézier curve.
+ ///
+ Bezier3 = Bezier
+}
diff --git a/src/Common/Drawing2D/PenAlignment.cs b/src/Common/Drawing2D/PenAlignment.cs
new file mode 100644
index 0000000..9b9d78f
--- /dev/null
+++ b/src/Common/Drawing2D/PenAlignment.cs
@@ -0,0 +1,32 @@
+namespace GeneXus.Drawing.Drawing2D;
+
+///
+/// Specifies the alignment of a object in relation to the theoretical, zero-width line.
+///
+public enum PenAlignment
+{
+ ///
+ /// Specifies the object is centered over the theoretical line.
+ ///
+ Center = 0,
+
+ ///
+ /// Specifies the is positioned on the inside of the theoretical line.
+ ///
+ Inset = 1,
+
+ ///
+ /// Specifies the is positioned on the outside of the theoretical line.
+ ///
+ Outset = 2,
+
+ ///
+ /// Specifies the is positioned on the left of the theoretical line.
+ ///
+ Left = 3,
+
+ ///
+ /// Specifies the is positioned on the right of the theoretical line.
+ ///
+ Right = 4
+}
diff --git a/src/Common/Drawing2D/PenType.cs b/src/Common/Drawing2D/PenType.cs
new file mode 100644
index 0000000..8562f79
--- /dev/null
+++ b/src/Common/Drawing2D/PenType.cs
@@ -0,0 +1,32 @@
+namespace GeneXus.Drawing.Drawing2D;
+
+///
+/// Specifies the type of fill a object uses to fill lines.
+///
+public enum PenType
+{
+ ///
+ /// Specifies a solid fill.
+ ///
+ SolidColor = 0,
+
+ ///
+ /// Specifies a hatch fill.
+ ///
+ HatchFill = 1,
+
+ ///
+ /// Specifies a bitmap texture fill.
+ ///
+ TextureFill = 2,
+
+ ///
+ /// Specifies a path gradient fill.
+ ///
+ PathGradient = 3,
+
+ ///
+ /// Specifies a linear gradient fill.
+ ///
+ LinearGradient = 4
+}
diff --git a/src/Common/Drawing2D/PixelOffsetMode.cs b/src/Common/Drawing2D/PixelOffsetMode.cs
new file mode 100644
index 0000000..c1cdf07
--- /dev/null
+++ b/src/Common/Drawing2D/PixelOffsetMode.cs
@@ -0,0 +1,38 @@
+namespace GeneXus.Drawing.Drawing2D;
+
+///
+/// Specifies how pixels are offset during rendering.
+///
+public enum PixelOffsetMode
+{
+ ///
+ /// Specifies an invalid mode.
+ ///
+ Invalid = -1,
+
+ ///
+ /// Specifies the default mode.
+ ///
+ Default = 0,
+
+ ///
+ /// Specifies high speed, low quality rendering.
+ ///
+ HighSpeed = 1,
+
+ ///
+ /// Specifies high quality, low speed rendering.
+ ///
+ HighQuality = 2,
+
+ ///
+ /// Specifies no pixel offset.
+ ///
+ None = 3,
+
+ ///
+ /// Specifies that pixels are offset by -0.5 units, both horizontally
+ /// and vertically, for high speed antialiasing.
+ ///
+ Half = 4
+}
diff --git a/src/Common/Drawing2D/RegionData.cs b/src/Common/Drawing2D/RegionData.cs
new file mode 100644
index 0000000..dd744bf
--- /dev/null
+++ b/src/Common/Drawing2D/RegionData.cs
@@ -0,0 +1,11 @@
+namespace GeneXus.Drawing.Drawing2D;
+
+public sealed class RegionData
+{
+ internal RegionData(byte[] data) => Data = data;
+
+ ///
+ /// Gets or sets an array of bytes that specify the object.
+ ///
+ public byte[] Data { get; set; }
+}
diff --git a/src/Common/Drawing2D/SmoothingMode.cs b/src/Common/Drawing2D/SmoothingMode.cs
new file mode 100644
index 0000000..a923776
--- /dev/null
+++ b/src/Common/Drawing2D/SmoothingMode.cs
@@ -0,0 +1,37 @@
+namespace GeneXus.Drawing.Drawing2D;
+
+///
+/// Specifies whether smoothing (antialiasing) is applied to lines and curves and the edges of filled areas.
+///
+public enum SmoothingMode
+{
+ ///
+ /// Specifies an invalid mode.
+ ///
+ Invalid = -1,
+
+ ///
+ /// Specifies no antialiasing.
+ ///
+ Default = 0,
+
+ ///
+ /// Specifies no antialiasing.
+ ///
+ HighSpeed = 1,
+
+ ///
+ /// Specifies antialiased rendering.
+ ///
+ HighQuality = 2,
+
+ ///
+ /// Specifies no antialiasing.
+ ///
+ None = 3,
+
+ ///
+ /// Specifies antialiased rendering.
+ ///
+ AntiAlias = 4
+}
diff --git a/src/Common/Drawing2D/WarpMode.cs b/src/Common/Drawing2D/WarpMode.cs
new file mode 100644
index 0000000..1c10419
--- /dev/null
+++ b/src/Common/Drawing2D/WarpMode.cs
@@ -0,0 +1,17 @@
+namespace GeneXus.Drawing.Drawing2D;
+
+///
+/// Specifies the type of warp transformation applied in a method.
+///
+public enum WarpMode
+{
+ ///
+ /// Specifies a perspective warp.
+ ///
+ Perspective = 0,
+
+ ///
+ /// Specifies a bilinear warp.
+ ///
+ Bilinear = 1
+}
diff --git a/src/Common/Drawing2D/WrapMode.cs b/src/Common/Drawing2D/WrapMode.cs
new file mode 100644
index 0000000..8c545b0
--- /dev/null
+++ b/src/Common/Drawing2D/WrapMode.cs
@@ -0,0 +1,32 @@
+namespace GeneXus.Drawing.Drawing2D;
+
+///
+/// Specifies how a texture or gradient is tiled when it is smaller than the area being filled.
+///
+public enum WrapMode
+{
+ ///
+ /// Tiles the gradient or texture.
+ ///
+ Tile = 0,
+
+ ///
+ /// Reverses the texture or gradient horizontally and then tiles the texture or gradient.
+ ///
+ TileFlipX = 1,
+
+ ///
+ /// Reverses the texture or gradient vertically and then tiles the texture or gradient.
+ ///
+ TileFlipY = 2,
+
+ ///
+ /// Reverses the texture or gradient horizontally and vertically and then tiles the texture or gradient.
+ ///
+ TileFlipXY = 3,
+
+ ///
+ /// The texture or gradient is not tiled.
+ ///
+ Clamp = 4
+}
diff --git a/src/Common/Font.cs b/src/Common/Font.cs
index 7b36ab0..2ea3bbc 100644
--- a/src/Common/Font.cs
+++ b/src/Common/Font.cs
@@ -288,7 +288,7 @@ void AppendStyle(string value)
///
/// The em-size, in points, of this .
[Browsable(false)]
- public float SizeInPoints => Size * GetFactor(DPI, Unit, GraphicsUnit.Point);
+ public float SizeInPoints => Size * Graphics.GetFactor(Graphics.DPI.Y, Unit, GraphicsUnit.Point);
///
/// Gets the unit of measure for this .
@@ -320,26 +320,29 @@ void AppendStyle(string value)
/// Returns the line spacing, in pixels, of this .
///
public float GetHeight()
- => GetHeight(DPI);
+ {
+ using var graphics = new Graphics(new SKBitmap(1, 1)) { PageUnit = Unit };
+ return GetHeight(graphics);
+ }
///
/// Returns the line spacing, in the current unit of a specified Graphics, of this .
///
- public float GetHeight(object graphics)
- => throw new NotImplementedException(); // TODO: will be replaced by GetHeight(graphics.PageUnit, graphics.DpiY) when Graphics class had been implemented
+ public float GetHeight(Graphics graphics)
+ => GetHeight(graphics.PageUnit, graphics.DpiY);
///
- /// Returns the height, in pixels, of this when drawn to a device with the specified vertical resolution.
+ /// Returns the height, in pixels, of this when drawn to a device with the specified vertical resolution.
///
public float GetHeight(float dpi)
=> GetHeight(Unit, dpi);
///
- /// Returns the height, in pixels, of this when drawn to a device with the specified vertical resolution
+ /// Returns the height, in pixels, of this when drawn to a device with the specified vertical resolution
/// and with the specified .
///
private float GetHeight(GraphicsUnit unit, float dpi)
- => (Metrics.Descent - Metrics.Ascent + Metrics.Leading) * GetFactor(dpi, unit, GraphicsUnit.Pixel);
+ => (Metrics.Descent - Metrics.Ascent + Metrics.Leading) * Graphics.GetFactor(dpi, unit, GraphicsUnit.Pixel);
///
/// Creates a from the specified handle to a device context (HDC).
@@ -391,7 +394,7 @@ public void ToLogFont(out Interop.LOGFONT logFont)
=> ToLogFont(out logFont, null);
///
- public void ToLogFont(out Interop.LOGFONT logFont, object graphics)
+ public void ToLogFont(out Interop.LOGFONT logFont, Graphics graphics)
=> ToLogFont(logFont = new Interop.LOGFONT(), graphics);
///
@@ -406,7 +409,7 @@ public void ToLogFont(object logFont)
///
/// An to represent the structure that this method creates.
/// A Graphics that provides additional information for the structure.
- public void ToLogFont(object logFont, object graphics)
+ public void ToLogFont(object logFont, Graphics graphics)
=> throw new NotSupportedException("unsupported by skia.");
#endregion
@@ -456,33 +459,5 @@ public static int GetFontCount(Stream stream)
private SKFontMetrics Metrics => Typeface.ToFont(Size).Metrics;
- private static int DPI
- {
- get
- {
- using var surface = SKSurface.Create(new SKImageInfo(50, 50));
- return (int)(100f * surface.Canvas.DeviceClipBounds.Width / surface.Canvas.LocalClipBounds.Width);
- }
- }
-
- private static float GetFactor(float dpi, GraphicsUnit sourceUnit, GraphicsUnit targetUnit)
- {
- float sourceFactor = GetPointFactor(sourceUnit, dpi);
- float targetFactor = GetPointFactor(targetUnit, dpi);
- return sourceFactor / targetFactor;
-
- static float GetPointFactor(GraphicsUnit unit, float dpi) => unit switch
- {
- GraphicsUnit.World => throw new NotSupportedException("World unit conversion is not supported."),
- GraphicsUnit.Display => 72 / dpi, // Assuming display unit is pixels
- GraphicsUnit.Pixel => 72 / dpi, // 1 pixel = 72 points per inch / Dots Per Inch
- GraphicsUnit.Point => 1, // Already in points
- GraphicsUnit.Inch => 72, // 1 inch = 72 points
- GraphicsUnit.Document => 72 / 300f, // 1 document unit = 1/300 inch
- GraphicsUnit.Millimeter => 72 / 25.4f, // 1 millimeter = 1/25.4 inch
- _ => throw new ArgumentException("Invalid GraphicsUnit")
- };
- }
-
#endregion
}
diff --git a/src/Common/Graphics.cs b/src/Common/Graphics.cs
new file mode 100644
index 0000000..64544c3
--- /dev/null
+++ b/src/Common/Graphics.cs
@@ -0,0 +1,1967 @@
+using System;
+using System.Numerics;
+using SkiaSharp;
+using GeneXus.Drawing.Drawing2D;
+using GeneXus.Drawing.Text;
+using System.Linq;
+using System.Collections.Generic;
+
+namespace GeneXus.Drawing;
+
+public sealed class Graphics : IDisposable
+{
+ internal readonly SKCanvas m_canvas;
+ internal readonly SKBitmap m_bitmap;
+ internal readonly SKPath m_path; // NOTE: tracks every shape added in order to implement IsVisible method
+ private int m_context;
+
+ internal static readonly (int X, int Y) DPI;
+
+ static Graphics()
+ {
+ using var surface = SKSurface.Create(new SKImageInfo(50, 50));
+ DPI.X = (int)(100f * surface.Canvas.DeviceClipBounds.Width / surface.Canvas.LocalClipBounds.Width);
+ DPI.Y = (int)(100f * surface.Canvas.DeviceClipBounds.Height / surface.Canvas.LocalClipBounds.Height);
+ }
+
+ internal Graphics(SKBitmap bitmap)
+ {
+ m_bitmap = bitmap;
+ m_canvas = new SKCanvas(m_bitmap);
+ m_path = new SKPath();
+ m_context = -1;
+
+ Clip = new Region(ClipBounds);
+ }
+
+ ///
+ /// Cleans up resources for this .
+ ///
+ ~Graphics() => Dispose(false);
+
+
+ #region IDisposable
+
+ ///
+ /// Cleans up resources for this .
+ ///
+ public void Dispose()
+ {
+ GC.SuppressFinalize(this);
+ Dispose(true);
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ using var surface = SKSurface.Create(m_bitmap.Info);
+ surface.Draw(m_canvas, 0, 0, new SKPaint() { Color = SKColors.Transparent });
+ surface.Canvas.ClipRegion(Clip.m_region);
+
+ using var image = surface.Snapshot();
+ using var bitmap = SKBitmap.FromImage(image);
+ m_bitmap.SetPixels(bitmap.GetPixels());
+ }
+ m_canvas.Dispose();
+ }
+
+ #endregion
+
+
+ #region Operations
+
+ ///
+ /// Creates a with the coordinates of the specified .
+ ///
+ public static explicit operator SKCanvas(Graphics graphic) => graphic.m_canvas;
+
+ #endregion
+
+
+ #region Properties
+
+ ///
+ /// Gets or sets a that limits the drawing region of this .
+ ///
+ public Region Clip
+ {
+ get => new Region(ClipRegion.m_region);
+ set => SetClip(value);
+ }
+
+ ///
+ /// Gets a structure that bounds the clipping region of this .
+ ///
+ public RectangleF ClipBounds => new(m_canvas.LocalClipBounds);
+
+ ///
+ /// Gets a value that specifies how composited images are drawn to this .
+ ///
+ public CompositingMode CompositingMode { get; set; } = CompositingMode.SourceOver;
+
+ ///
+ /// Gets or sets the rendering quality of composited images drawn to this .
+ ///
+ public CompositingQuality CompositingQuality { get; set; } = CompositingQuality.Default;
+
+ ///
+ /// Gets the horizontal resolution of this .
+ ///
+ public float DpiX => DPI.X;
+
+ ///
+ /// Gets the vertical resolution of this .
+ ///
+ public float DpiY => DPI.Y;
+
+ ///
+ /// Gets or sets the interpolation mode associated with this .
+ ///
+ public InterpolationMode InterpolationMode { get; set; } = InterpolationMode.Bilinear;
+
+ ///
+ /// Gets a value indicating whether the clipping region of this is empty.
+ ///
+ public bool IsClipEmpty => m_canvas.IsClipEmpty;
+
+ ///
+ /// Gets a value indicating whether the visible clipping region of this is empty.
+ ///
+ public bool IsVisibleClipEmpty => m_canvas.LocalClipBounds is { Width: <= 0, Height: <= 0 };
+
+ ///
+ /// Gets or sets the scaling between world units and page units for this .
+ ///
+ public float PageScale { get; set; } = 1;
+
+ ///
+ /// Gets or sets the unit of measure used for page coordinates in this .
+ ///
+ public GraphicsUnit PageUnit { get; set; } = GraphicsUnit.Display;
+
+ ///
+ /// Gets or sets a value specifying how pixels are offset during rendering of this .
+ ///
+ public PixelOffsetMode PixelOffsetMode { get; set; } = PixelOffsetMode.Default; // TODO: to be implemented
+
+ ///
+ /// Gets or sets the rendering origin of this for dithering and for hatch brushes.
+ ///
+ public Point RenderingOrigin { get; set; } = new Point(0, 0);
+
+ ///
+ /// Gets or sets the rendering quality for this .
+ ///
+ public SmoothingMode SmoothingMode { get; set; } = SmoothingMode.None;
+
+ ///
+ /// Gets or sets the gamma correction value for rendering text.
+ ///
+ public int TextContrast { get; set; } = 4; // TODO: to be implemented
+
+ ///
+ /// Gets or sets the rendering mode for text associated with this .
+ ///
+ public TextRenderingHint TextRenderingHint { get; set; } = TextRenderingHint.SystemDefault; // TODO: to be implemented
+
+ ///
+ /// Gets or sets a copy of the geometric world transformation for this .
+ ///
+ public Matrix Transform
+ {
+ get => new(m_canvas.TotalMatrix);
+ set => m_canvas.SetMatrix(value.m_matrix);
+ }
+
+ ///
+ /// Gets or sets the world transform elements for this .
+ ///
+ public Matrix3x2 TransformElements
+ {
+ get => Transform.MatrixElements;
+ set => Transform.MatrixElements = value;
+ }
+
+ ///
+ /// Gets the bounding rectangle of the visible clipping region of this .
+ ///
+ public RectangleF VisibleClipBounds => Clip.GetBounds(this);
+
+ #endregion
+
+
+ #region Factory
+
+ ///
+ /// Creates a new from the specified handle to a device
+ /// context.
+ ///
+ public static Graphics FromHdc(IntPtr hdc)
+ => FromHdc(hdc, IntPtr.Zero);
+
+ ///
+ /// Creates a new from the specified handle to a device
+ /// context and handle to a device.
+ ///
+ public static Graphics FromHdc(IntPtr hdc, IntPtr hdevice)
+ => throw new NotImplementedException();
+
+ ///
+ /// Returns a for the specified device context.
+ ///
+ public static Graphics FromHdcInternal(IntPtr hdc)
+ => throw new NotImplementedException();
+
+ ///
+ /// Creates a new from the specified handle to a window.
+ ///
+ public static Graphics FromHwnd(IntPtr hwnd)
+ => throw new NotImplementedException();
+
+ ///
+ /// Creates a new for the specified windows handle.
+ ///
+ public static Graphics FromHwndInternal(IntPtr hwnd)
+ => throw new NotImplementedException();
+
+ ///
+ /// Creates a new from the specified .
+ ///
+ public static Graphics FromImage(Image image)
+ {
+ var bitmap = image is Bitmap bm ? bm.m_bitmap : SKBitmap.FromImage(image.InnerImage);
+ var graphics = new Graphics(bitmap);
+ graphics.DrawImage(image, 0, 0);
+ return graphics;
+ }
+
+ #endregion
+
+
+ #region Methods
+
+ ///
+ /// Adds a comment to the current .
+ ///
+ public void AddMetafileComment(byte[] data)
+ => throw new NotImplementedException();
+
+ ///
+ /// Saves a graphics container with the current state of this and
+ /// and opens and uses a new graphics container.
+ ///
+ public GraphicsContainer BeginContainer()
+ {
+ var rect = new RectangleF(m_canvas.LocalClipBounds);
+ return BeginContainer(rect, rect, GraphicsUnit.Pixel);
+ }
+
+ ///
+ /// Saves a graphics container with the current state of this and
+ /// opens and uses a new graphics container with the specified scale transformation.
+ ///
+ public GraphicsContainer BeginContainer(RectangleF dstRect, RectangleF srcRect, GraphicsUnit unit)
+ {
+ int state = m_canvas.Save();
+
+ float factorX = GetFactor(DpiX, unit, GraphicsUnit.Pixel);
+ float factorY = GetFactor(DpiY, unit, GraphicsUnit.Pixel);
+
+ var src = new RectangleF(srcRect.X, srcRect.Y, srcRect.Width * factorX, srcRect.Height * factorY);
+ var dst = new RectangleF(dstRect.X, dstRect.Y, dstRect.Width * factorX, dstRect.Height * factorY);
+
+ float scaleX = dst.Width / src.Width;
+ float scaleY = dst.Height / src.Height;
+
+ float translateX = dst.Left - src.Left * scaleX;
+ float translateY = dst.Top - src.Top * scaleY;
+
+ m_canvas.ResetMatrix();
+ m_canvas.Translate(translateX, translateY);
+ m_canvas.Scale(scaleX, scaleY);
+
+ // TODO: reset all properties of this instance and restore them when calling to EndContainer
+ return new GraphicsContainer(state);
+ }
+
+ ///
+ /// Saves a graphics container with the current state of this and
+ /// opens and uses a new graphics container with the specified scale transformation.
+ ///
+ public GraphicsContainer BeginContainer(Rectangle dstRect, Rectangle srcRect, GraphicsUnit unit)
+ => BeginContainer(new RectangleF(dstRect.m_rect), new RectangleF(srcRect.m_rect), unit);
+
+ ///
+ /// Clears the entire drawing surface and fills it with a transparent background color.
+ ///
+ public void Clear()
+ => Clear(Color.Transparent);
+
+ ///
+ /// Clears the entire drawing surface and fills it with the specified background color.
+ ///
+ public void Clear(Color color)
+ {
+ ClipColor = color;
+ m_canvas.Clear(ClipColor.m_color);
+ }
+
+ ///
+ /// Performs a bit-block transfer of the color data, corresponding to a rectangle of pixels,
+ /// from the screen to the drawing surface of the .
+ ///
+ public void CopyFromScreen(Point srcUpperLeft, Point dstUpperLeft, Size blockRegionSize, CopyPixelOperation copyPixelOperation = CopyPixelOperation.SourceCopy)
+ => CopyFromScreen(srcUpperLeft.X, srcUpperLeft.Y, dstUpperLeft.X, dstUpperLeft.Y, blockRegionSize, copyPixelOperation);
+
+ ///
+ /// Performs a bit-block transfer of the color data, corresponding to a rectangle of pixels,
+ /// from the screen to the drawing surface of the .
+ ///
+ public void CopyFromScreen(int srcX, int srcY, int dstX, int dstY, Size blockRegionSize, CopyPixelOperation copyPixelOperation = CopyPixelOperation.SourceCopy)
+ => throw new NotSupportedException("skia unsupported feature");
+
+ ///
+ /// Draws an arc representing a portion of an ellipse specified by a structure.
+ ///
+ public void DrawArc(Pen pen, RectangleF oval, float startAngle, float sweepAngle)
+ {
+ using var path = new GraphicsPath();
+ path.AddArc(oval, startAngle, sweepAngle);
+ DrawPath(pen, path);
+ }
+
+ ///
+ /// Draws an arc representing a portion of an ellipse specified by a pair of coordinates, a width, and a height.
+ ///
+ public void DrawArc(Pen pen, float x, float y, float width, float height, float startAngule, float sweepAngle)
+ => DrawArc(pen, new RectangleF(x, y, width, height), startAngule, sweepAngle);
+
+ ///
+ /// Draws an arc representing a portion of an ellipse specified by a structure.
+ ///
+ public void DrawArc(Pen pen, Rectangle oval, float startAngle, float sweepAngle)
+ => DrawArc(pen, new RectangleF(oval.m_rect), startAngle, sweepAngle);
+
+ ///
+ /// Draws an arc representing a portion of an ellipse specified by a pair of coordinates, a width, and a height.
+ ///
+ public void DrawArc(Pen pen, int x, int y, int width, int height, float startAngule, float sweepAngle)
+ => DrawArc(pen, new Rectangle(x, y, width, height), startAngule, sweepAngle);
+
+ ///
+ /// Draws a cubic Bezier curve defined by four points.
+ ///
+ public void DrawBezier(Pen pen, PointF pt1, PointF pt2, PointF pt3, PointF pt4)
+ => DrawBeziers(pen, new[] { pt1, pt2, pt3, pt4 });
+
+ ///
+ /// Draws a cubic Bezier curve defined by four ordered pairs that represent points.
+ ///
+ public void DrawBezier(Pen pen, float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4)
+ => DrawBezier(pen, new PointF(x1, y1), new PointF(x2, y2), new PointF(x3, y3), new PointF(x4, y4));
+
+ ///
+ /// Draws a cubic Bezier curve defined by four points.
+ ///
+ public void DrawBezier(Pen pen, Point pt1, Point pt2, Point pt3, Point pt4)
+ => DrawBeziers(pen, new[] { pt1, pt2, pt3, pt4 });
+
+ ///
+ /// Draws a cubic Bezier curve defined by four ordered pairs that represent points.
+ ///
+ public void DrawBezier(Pen pen, int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4)
+ => DrawBezier(pen, new Point(x1, y1), new Point(x2, y2), new Point(x3, y3), new Point(x4, y4));
+
+ ///
+ /// Draws a series of Bézier splines from an array of structures.
+ ///
+ public void DrawBeziers(Pen pen, params PointF[] points)
+ {
+ if (points.Length < 4 || points.Length % 3 != 1)
+ throw new ArgumentException("invalid number of points for drawing Bezier curves", nameof(points));
+
+ using var path = new GraphicsPath();
+ path.AddBeziers(points);
+ DrawPath(pen, path);
+ }
+
+ ///
+ /// Draws a series of Bézier splines from an array of structures.
+ ///
+ public void DrawBeziers(Pen pen, params Point[] points)
+ => DrawBeziers(pen, Array.ConvertAll(points, point => new PointF(point.m_point)));
+
+ ///
+ /// Draws the given .
+ ///
+ public void DrawCachedBitmap(object cachedBitmap, int x, int y)
+ => throw new NotImplementedException();
+
+ ///
+ /// Draws a closed cardinal spline defined by an array of structures using a specified tension
+ ///
+ public void DrawClosedCurve(Pen pen, PointF[] points, float tension = 0.5f, FillMode fillMode = FillMode.Winding)
+ {
+ using var path = GetCurvePath(points, fillMode, tension, true);
+ DrawPath(pen, path);
+ }
+
+ ///
+ /// Draws a closed cardinal spline defined by an array of structures using a specified tension
+ ///
+ public void DrawClosedCurve(Pen pen, Point[] points, float tension = 0.5f, FillMode fillMode = FillMode.Winding)
+ => DrawClosedCurve(pen, Array.ConvertAll(points, point => new PointF(point.m_point)), tension, fillMode);
+
+ ///
+ /// Draws a cardinal spline through a specified array of structures using a specified tension. The drawing
+ /// begins offset from the beginning of the array.
+ ///
+ public void DrawCurve(Pen pen, PointF[] points, int offset, int numberOfSegments, float tension = 0.5f)
+ {
+ if (offset < 0 || offset >= points.Length)
+ throw new ArgumentOutOfRangeException(nameof(offset));
+
+ if (numberOfSegments < 1 || offset + numberOfSegments > points.Length)
+ throw new ArgumentOutOfRangeException(nameof(numberOfSegments));
+
+ var fillMode = FillMode.Alternate;
+ points = points.Skip(offset).Take(numberOfSegments).ToArray();
+
+ using var path = GetCurvePath(points, fillMode, tension, false);
+ DrawPath(pen, path);
+ }
+
+ ///
+ /// Draws a cardinal spline through a specified array of structures using a specified tension. The drawing
+ /// begins offset from the beginning of the array.
+ ///
+ public void DrawCurve(Pen pen, Point[] points, int offset, int numberOfSegments, float tension = 0.5f)
+ => DrawCurve(pen, Array.ConvertAll(points, point => new PointF(point.m_point)), offset, numberOfSegments, tension);
+
+ ///
+ /// Draws a cardinal spline through a specified array of structures using a specified tension.
+ ///
+ public void DrawCurve(Pen pen, PointF[] points, float tension = 0.5f)
+ => DrawCurve(pen, points, 0, points.Length, tension);
+
+ ///
+ /// Draws a cardinal spline through a specified array of structures using a specified tension.
+ ///
+ public void DrawCurve(Pen pen, Point[] points, float tension = 0.5f)
+ => DrawCurve(pen, Array.ConvertAll(points, point => new PointF(point.m_point)), 0, points.Length, tension);
+
+ ///
+ /// Draws an ellipse defined by a bounding rectangle specified by coordinates for the upper-left corner of the
+ /// rectangle, a height, and a width.
+ ///
+ public void DrawEllipse(Pen pen, float x, float y, float width, float height)
+ => DrawEllipse(pen, new RectangleF(x, y, width, height));
+
+ ///
+ /// Draws an ellipse specified by a bounding structure.
+ ///
+ public void DrawEllipse(Pen pen, RectangleF rect)
+ {
+ using var path = GetEllipsePath(rect);
+ DrawPath(pen, path);
+ }
+
+ ///
+ /// Draws an ellipse defined by a bounding rectangle specified by coordinates for the upper-left corner of the
+ /// rectangle, a height, and a width.
+ ///
+ public void DrawEllipse(Pen pen, int x, int y, int width, int height)
+ => DrawEllipse(pen, new Rectangle(x, y, width, height));
+
+ ///
+ /// Draws an ellipse specified by a bounding structure.
+ ///
+ public void DrawEllipse(Pen pen, Rectangle rect)
+ => DrawEllipse(pen, new RectangleF(rect.m_rect));
+
+ ///
+ /// Draws the image represented by the specified within the area specified
+ /// by a structure.
+ ///
+ public void DrawIcon(Icon icon, Rectangle rect)
+ => DrawImage(icon.ToBitmap(), rect);
+
+ ///
+ /// Draws the image represented by the specified at the specified coordinates.
+ ///
+ public void DrawIcon(Icon icon, int x, int y)
+ => DrawImage(icon.ToBitmap(), x, y);
+
+ ///
+ /// Draws the image represented by the specified without scaling the image.
+ ///
+ public void DrawIconUnstretched(Icon icon, Rectangle rect)
+ => DrawImageUnscaled(icon.ToBitmap(), rect);
+
+ ///
+ /// Draws the specified , using its original physical size, at the specified
+ /// location given by a structure.
+ ///
+ public void DrawImage(Image image, PointF point)
+ => DrawImage(image, point.X, point.Y);
+
+ ///
+ /// Draws the specified , using its original physical size, at the specified location.
+ ///
+ public void DrawImage(Image image, float x, float y)
+ => DrawImage(image, x, y, image.Width, image.Height);
+
+ ///
+ /// Draws the specified , using its original physical size, at the specified
+ /// location given by a structure.
+ ///
+ public void DrawImage(Image image, Point point)
+ => DrawImage(image, new PointF(point.m_point));
+
+ ///
+ /// Draws the specified , using its original physical size, at the specified location.
+ ///
+ public void DrawImage(Image image, int x, int y)
+ => DrawImage(image, x, y, image.Width, image.Height);
+
+ ///
+ /// Draws the specified at the specified location and with the specified
+ /// size given by a structure.
+ ///
+ public void DrawImage(Image image, RectangleF rect)
+ => m_canvas.DrawImage(image.InnerImage, rect.m_rect);
+
+ ///
+ /// Draws the specified at the specified location and with the specified size.
+ ///
+ public void DrawImage(Image image, float x, float y, float width, float height)
+ => DrawImage(image, new RectangleF(x, y, width, height));
+
+ ///
+ /// Draws the specified at the specified location and with the specified
+ /// size given by a structure.
+ ///
+ public void DrawImage(Image image, Rectangle rect)
+ => DrawImage(image, new RectangleF(rect.m_rect));
+
+ ///
+ /// Draws the specified at the specified location and with the specified size.
+ ///
+ public void DrawImage(Image image, int x, int y, int width, int height)
+ => DrawImage(image, new Rectangle(x, y, width, height));
+
+ ///
+ /// Draws the specified at the specified location and with the specified
+ /// shape and size.
+ ///
+ public void DrawImage(Image image, PointF[] points)
+ {
+ var unit = GraphicsUnit.Pixel;
+ var bounds = image.GetBounds(ref unit);
+ DrawImage(image, points, new RectangleF(bounds.m_rect), unit);
+ }
+
+ ///
+ /// Draws the specified at the specified location and with the specified
+ /// shape and size.
+ ///
+ public void DrawImage(Image image, Point[] points)
+ => DrawImage(image, Array.ConvertAll(points, point => new PointF(point.m_point)));
+
+ ///
+ /// Draws a portion of an at a specified location.
+ ///
+ public void DrawImage(Image image, float x, float y, RectangleF rect, GraphicsUnit unit)
+ => DrawImage(image, new RectangleF(x, y, rect.Width, rect.Height), rect, unit);
+
+ ///
+ /// Draws a portion of an at a specified location.
+ ///
+ public void DrawImage(Image image, int x, int y, Rectangle rect, GraphicsUnit unit)
+ => DrawImage(image, new RectangleF(x, y, rect.Width, rect.Height), rect, unit);
+
+ ///
+ /// Draws the specified portion of the specified at the specified location
+ /// and with the specified size.
+ ///
+ public void DrawImage(Image image, RectangleF destination, RectangleF source, GraphicsUnit unit)
+ {
+ var dst = new PointF[]
+ {
+ new(destination.Left, destination.Top),
+ new(destination.Right, destination.Top),
+ new(destination.Right, destination.Bottom),
+ new(destination.Left, destination.Bottom)
+ };
+ DrawImage(image, dst, source, unit);
+ }
+
+ ///
+ /// Draws the specified portion of the specified at the specified location
+ /// and with the specified size.
+ ///
+ public void DrawImage(Image image, Rectangle destination, Rectangle source, GraphicsUnit unit)
+ => DrawImage(image, new RectangleF(destination.m_rect), new RectangleF(source.m_rect), unit);
+
+ ///
+ /// Draws a portion of an at a specified location and with the specified size.
+ ///
+ public void DrawImage(Image image, PointF[] destPoints, RectangleF srcRect, GraphicsUnit srcUnit, object imageAtt = null, object callback = null, int callbackData = 0)
+ {
+ // TODO: Implement ImageAttributes (attributes), DrawImageAbort (callback) and IntPtr (callbackData)
+ using var path = new GraphicsPath();
+ path.AddLines(destPoints);
+ path.CloseFigure();
+
+ m_path.AddPath(path.m_path);
+ m_canvas.ClipPath(path.m_path);
+
+ var dstRect = path.GetBounds();
+
+ float factorX = GetFactor(DpiX, srcUnit, GraphicsUnit.Pixel);
+ float factorY = GetFactor(DpiY, srcUnit, GraphicsUnit.Pixel);
+
+ var src = new RectangleF(srcRect.X, srcRect.Y, srcRect.Width * factorX, srcRect.Height * factorY);
+ var dst = new RectangleF(dstRect.X, dstRect.Y, dstRect.Width * factorX, dstRect.Height * factorY);
+
+ m_canvas.DrawImage(image.InnerImage, src.m_rect, dst.m_rect);
+ }
+
+ ///
+ /// Draws a portion of an at a specified location and with the specified size.
+ ///
+ public void DrawImage(Image image, Point[] destPoints, Rectangle srcRect, GraphicsUnit srcUnit, object imageAtt = null, object callback = null, int callbackData = 0)
+ => DrawImage(image, Array.ConvertAll(destPoints, point => new PointF(point.m_point)), new RectangleF(srcRect.m_rect), srcUnit, imageAtt, callback, callbackData);
+
+ ///
+ /// Draws the specified portion of the specified at the specified location and with the specified size.
+ ///
+ public void DrawImage(Image image, RectangleF destRect, float srcX, float srcY, float srcWidth, float srcHeight, GraphicsUnit srcUnit, object imageAttrs = null, object callback = null)
+ => DrawImage(image, destRect, srcX, srcY, srcWidth, srcHeight, srcUnit, imageAttrs, callback, IntPtr.Zero);
+
+ ///
+ /// Draws the specified portion of the specified at the specified location and with the specified size.
+ ///
+ public void DrawImage(Image image, RectangleF destRect, float srcX, float srcY, float srcWidth, float srcHeight, GraphicsUnit srcUnit, object imageAttrs, object callback, IntPtr callbackData)
+ {
+ var destPoints = new PointF[]
+ {
+ new(destRect.Left, destRect.Top),
+ new(destRect.Right, destRect.Top),
+ new(destRect.Right, destRect.Bottom),
+ new(destRect.Left, destRect.Bottom),
+ };
+ DrawImage(image, destPoints, new RectangleF(srcX, srcY, srcWidth, srcHeight), srcUnit, imageAttrs, callback, unchecked((int)callbackData));
+ }
+
+ ///
+ /// Draws the specified portion of the specified at the specified location and with the specified size.
+ ///
+ public void DrawImage(Image image, Rectangle destRect, int srcX, int srcY, int srcWidth, int srcHeight, GraphicsUnit srcUnit, object imageAttrs = null, object callback = null)
+ => DrawImage(image, destRect, srcX, srcY, srcWidth, srcHeight, srcUnit, imageAttrs, callback, IntPtr.Zero);
+
+ ///
+ /// Draws the specified portion of the specified at the specified location and with the specified size.
+ ///
+ public void DrawImage(Image image, Rectangle destRect, int srcX, int srcY, int srcWidth, int srcHeight, GraphicsUnit srcUnit, object imageAttrs, object callback, IntPtr callbackData)
+ => DrawImage(image, new RectangleF(destRect.m_rect), srcX, srcY, srcWidth, srcHeight, srcUnit, imageAttrs, callback, callbackData);
+
+ ///
+ /// Draws the specified using its original physical size at a specified location.
+ ///
+ public void DrawImageUnscaled(Image image, Point point)
+ => DrawImageUnscaled(image, point.X, point.Y);
+
+ ///
+ /// Draws the specified using its original physical size at the location specified by a coordinate pair.
+ ///
+ public void DrawImageUnscaled(Image image, int x, int y)
+ => DrawImageUnscaled(image, x, y, image.Width, image.Height);
+
+ ///
+ /// Draws the specified using its original physical size at a specified location.
+ ///
+ public void DrawImageUnscaled(Image image, Rectangle rect)
+ => DrawImage(image, rect);
+
+ ///
+ /// Draws the specified using its original physical size at a specified location.
+ ///
+ public void DrawImageUnscaled(Image image, int x, int y, int width, int height)
+ => DrawImageUnscaled(image, new Rectangle(x, y, width, height));
+
+ ///
+ /// Draws the specified without scaling and clips it, if necessary, to fit in the specified rectangle.
+ ///
+ public void DrawImageUnscaledAndClipped(Image image, Rectangle rect)
+ => DrawImage(image, rect, rect, GraphicsUnit.Pixel);
+
+ ///
+ /// Draws a line connecting the two points specified by the coordinate pairs.
+ ///
+ public void DrawLine(Pen pen, float x1, float y1, float x2, float y2)
+ => DrawLine(pen, new PointF(x1, y1), new PointF(x2, y2));
+
+ ///
+ /// Draws a line connecting two structures.
+ ///
+ public void DrawLine(Pen pen, PointF point1, PointF point2)
+ {
+ using var path = new GraphicsPath();
+ path.AddLine(point1, point2);
+ DrawPath(pen, path);
+ }
+
+ ///
+ /// Draws a line connecting the two points specified by the coordinate pairs.
+ ///
+ public void DrawLine(Pen pen, int x1, int y1, int x2, int y2)
+ => DrawLine(pen, new Point(x1, y1), new Point(x2, y2));
+
+ ///
+ /// Draws a line connecting two structures.
+ ///
+ public void DrawLine(Pen pen, Point point1, Point point2)
+ => DrawLine(pen, new PointF(point1.m_point), new PointF(point2.m_point));
+
+ ///
+ /// Draws a series of line segments that connect an array of structures.
+ ///
+ public void DrawLines(Pen pen, params PointF[] points)
+ {
+ if (points.Length < 2)
+ throw new ArgumentException("must define at least 2 points", nameof(points));
+ for (int i = 1; i < points.Length; i++)
+ DrawLine(pen, points[i - 1], points[i]);
+ }
+
+ ///
+ /// Draws a series of line segments that connect an array of structures.
+ ///
+ public void DrawLines(Pen pen, params Point[] points)
+ => DrawLines(pen, Array.ConvertAll(points, point => new PointF(point.m_point)));
+
+ ///
+ /// Draws a .
+ ///
+ public void DrawPath(Pen pen, GraphicsPath path)
+ => PaintPath(path.m_path, pen.m_paint);
+
+ ///
+ /// Draws a pie shape defined by an ellipse specified by a structure and two radial lines.
+ ///
+ public void DrawPie(Pen pen, RectangleF rect, float startAngle, float sweepAngle)
+ {
+ using var path = GetPiePath(rect, startAngle, sweepAngle);
+ DrawPath(pen, path);
+ }
+
+ ///
+ /// Draws a pie shape defined by an ellipse specified by a structure and two radial lines.
+ ///
+ public void DrawPie(Pen pen, Rectangle rect, float startAngle, float sweepAngle)
+ => DrawPie(pen, new RectangleF(rect.m_rect), startAngle, sweepAngle);
+
+ ///
+ /// Draws a pie shape defined by an ellipse specified by a coordinate pair, a width, a height, and two radial lines.
+ ///
+ public void DrawPie(Pen pen, float x, float y, float width, float height, float startAngle, float sweepAngle)
+ => DrawPie(pen, new RectangleF(x, y, width, height), startAngle, sweepAngle);
+
+ ///
+ /// Draws a pie shape defined by an ellipse specified by a coordinate pair, a width, a height, and two radial lines.
+ ///
+ public void DrawPie(Pen pen, int x, int y, int width, int height, float startAngle, float sweepAngle)
+ => DrawPie(pen, new Rectangle(x, y, width, height), startAngle, sweepAngle);
+
+ ///
+ /// Draws a polygon defined by an array of structures.
+ ///
+ public void DrawPolygon(Pen pen, params PointF[] points)
+ {
+ using var path = GetPolygonPath(points, FillMode.Winding);
+ DrawPath(pen, path);
+ }
+
+ ///
+ /// Draws a polygon defined by an array of structures.
+ ///
+ public void DrawPolygon(Pen pen, params Point[] points)
+ => DrawPolygon(pen, Array.ConvertAll(points, point => new PointF(point.m_point)));
+
+ ///
+ /// Draws a rectangle specified by a structure.
+ ///
+ public void DrawRectangle(Pen pen, RectangleF rect)
+ {
+ using var path = GetRectanglePath(rect);
+ DrawPath(pen, path);
+ }
+
+ ///
+ /// Draws a rectangle specified by a structure.
+ ///
+ public void DrawRectangle(Pen pen, Rectangle rect)
+ => DrawRectangle(pen, new RectangleF(rect.m_rect));
+
+ ///
+ /// Draws a rectangle specified by a coordinate pair, a width, and a height.
+ ///
+ public void DrawRectangle(Pen pen, float x, float y, float witdth, float height)
+ => DrawRectangle(pen, new RectangleF(x, y, witdth, height));
+
+ ///
+ /// Draws a rectangle specified by a coordinate pair, a width, and a height.
+ ///
+ public void DrawRectangle(Pen pen, int x, int y, int witdth, int height)
+ => DrawRectangle(pen, new Rectangle(x, y, witdth, height));
+
+ ///
+ /// Draws a series of rectangles specified by structures.
+ ///
+ public void DrawRectangles(Pen pen, RectangleF[] rects)
+ => Array.ForEach(rects, rect => DrawRectangle(pen, rect));
+
+ ///
+ /// Draws a series of rectangles specified by structures.
+ ///
+ public void DrawRectangles(Pen pen, Rectangle[] rects)
+ => DrawRectangles(pen, Array.ConvertAll(rects, rect => new RectangleF(rect.m_rect)));
+
+ ///
+ /// Draws the specified text at the specified location with the specified and
+ /// objects using the formatting attributes of the specified StringFormat.
+ ///
+ public void DrawString(ReadOnlySpan text, Font font, Brush brush, float x, float y, StringFormat format = null)
+ => DrawString(new string(text.ToArray()), font, brush, x, y, format);
+
+ ///
+ /// Draws the specified text at the specified location with the specified and
+ /// objects using the formatting attributes of the specified StringFormat.
+ ///
+ public void DrawString(string text, Font font, Brush brush, float x, float y, StringFormat format = null)
+ => DrawString(text, font, brush, new PointF(x, y), format);
+
+ ///
+ /// Draws the specified text string in the specified location with the specified and
+ /// objects using the formatting attributes of the specified StringFormat.
+ /// text, Font font, Brush brush, PointF point, StringFormat format = null)
+ => DrawString(new string(text.ToArray()), font, brush, point, format);
+
+ ///
+ /// Draws the specified text string in the specified location with the specified and
+ /// objects using the formatting attributes of the specified StringFormat.
+ /// DrawString(text, font, brush, new RectangleF(point, float.PositiveInfinity, float.PositiveInfinity), format);
+
+ ///
+ /// Draws the specified text string in the specified rectangle with the specified and
+ /// objects using the formatting attributes of the specified StringFormat.
+ ///
+ public void DrawString(ReadOnlySpan text, Font font, Brush brush, RectangleF layout, StringFormat format = null)
+ => DrawString(new string(text.ToArray()), font, brush, layout, format);
+
+ ///
+ /// Draws the specified text string in the specified rectangle with the specified and
+ /// objects using the formatting attributes of the specified StringFormat.
+ ///
+ public void DrawString(string text, Font font, Brush brush, RectangleF layout, StringFormat format = null)
+ {
+ float emSize = DpiY * GetFactor(font.Size, font.Unit, GraphicsUnit.Pixel);
+
+ using var path = new GraphicsPath();
+ path.AddString(text, font.FontFamily, (int)font.Style, emSize, layout, format);
+ FillPath(brush, path); // NOTE: path has points defined by float values, but skia does not support subpixel path drawing
+ }
+
+ ///
+ /// Closes the current graphics container and restores the state of this to
+ /// the state saved by a call to the method.
+ ///
+ public void EndContainer(GraphicsContainer container)
+ => m_canvas.RestoreToCount(container.m_state);
+
+ ///
+ /// Sends the records in the specified Metafile, one at a time, to a callback method
+ /// for display at a specified point.
+ ///
+ public void EnumerateMetafile(object metafile, PointF destPoint, object callback)
+ => EnumerateMetafile(metafile, destPoint, callback, IntPtr.Zero);
+
+ ///
+ /// Sends the records in the specified Metafile, one at a time, to a callback method
+ /// for display at a specified point.
+ ///
+ public void EnumerateMetafile(object metafile, PointF destPoint, object callback, IntPtr callbackData)
+ => EnumerateMetafile(metafile, destPoint, callback, callbackData, null);
+
+ ///
+ /// Sends the records in the specified Metafile, one at a time, to a callback method
+ /// for display at a specified point.
+ ///
+ public void EnumerateMetafile(object metafile, Point destPoint, object callback)
+ => EnumerateMetafile(metafile, destPoint, callback, IntPtr.Zero);
+
+ ///
+ /// Sends the records in the specified Metafile, one at a time, to a callback method
+ /// for display at a specified point.
+ ///
+ public void EnumerateMetafile(object metafile, Point destPoint, object callback, IntPtr callbackData)
+ => EnumerateMetafile(metafile, destPoint, callback, callbackData, null);
+
+ ///
+ /// Sends the records of the specified Metafile, one at a time, to a callback method
+ /// for display in a specified rectangle.
+ ///
+ public void EnumerateMetafile(object metafile, RectangleF destRect, object callback)
+ => EnumerateMetafile(metafile, destRect, callback, IntPtr.Zero);
+
+ ///
+ /// Sends the records of the specified Metafile, one at a time, to a callback method
+ /// for display in a specified rectangle.
+ ///
+ public void EnumerateMetafile(object metafile, RectangleF destRect, object callback, IntPtr callbackData)
+ => EnumerateMetafile(metafile, destRect, callback, callbackData, null);
+
+ ///
+ /// Sends the records of the specified Metafile, one at a time, to a callback method
+ /// for display in a specified rectangle.
+ ///
+ public void EnumerateMetafile(object metafile, Rectangle destRect, object callback)
+ => EnumerateMetafile(metafile, destRect, callback, IntPtr.Zero);
+
+ ///
+ /// Sends the records of the specified Metafile, one at a time, to a callback method
+ /// for display in a specified rectangle.
+ ///
+ public void EnumerateMetafile(object metafile, Rectangle destRect, object callback, IntPtr callbackData)
+ => EnumerateMetafile(metafile, destRect, callback, callbackData, null);
+
+ ///
+ /// Sends the records in the specified Metafile, one at a time, to a callback method
+ /// for display in a specified parallelogram.
+ ///
+ public void EnumerateMetafile(object metafile, PointF[] destPoints, object callback)
+ => EnumerateMetafile(metafile, destPoints, callback, IntPtr.Zero);
+
+ ///
+ /// Sends the records in the specified Metafile, one at a time, to a callback method
+ /// for display in a specified parallelogram.
+ ///
+ public void EnumerateMetafile(object metafile, PointF[] destPoints, object callback, IntPtr callbackData)
+ => EnumerateMetafile(metafile, destPoints, callback, callbackData, null);
+
+ ///
+ /// Sends the records in the specified Metafile, one at a time, to a callback method
+ /// for display in a specified parallelogram.
+ ///
+ public void EnumerateMetafile(object metafile, Point[] destPoints, object callback)
+ => EnumerateMetafile(metafile, destPoints, callback, IntPtr.Zero);
+
+ ///
+ /// Sends the records in the specified Metafile, one at a time, to a callback method
+ /// for display in a specified parallelogram.
+ ///
+ public void EnumerateMetafile(object metafile, Point[] destPoints, object callback, IntPtr callbackData)
+ => EnumerateMetafile(metafile, destPoints, callback, callbackData, null);
+
+ ///
+ /// Sends the records in a selected rectangle from a Metafile, one at a time, to a callback
+ /// method for display at a specified point.
+ ///
+ public void EnumerateMetafile(object metafile, PointF destPoint, RectangleF srcRect, GraphicsUnit srcUnit, object callback)
+ => EnumerateMetafile(metafile, destPoint, srcRect, srcUnit, callback, IntPtr.Zero);
+
+ ///
+ /// Sends the records in a selected rectangle from a Metafile, one at a time, to a callback
+ /// method for display at a specified point.
+ ///
+ public void EnumerateMetafile(object metafile, PointF destPoint, RectangleF srcRect, GraphicsUnit srcUnit, object callback, IntPtr callbackData)
+ => EnumerateMetafile(metafile, destPoint, srcRect, srcUnit, callback, callbackData, null);
+
+ ///
+ /// Sends the records in a selected rectangle from a Metafile, one at a time, to a callback
+ /// method for display at a specified point.
+ ///
+ public void EnumerateMetafile(object metafile, Point destPoint, Rectangle srcRect, GraphicsUnit srcUnit, object callback)
+ => EnumerateMetafile(metafile, destPoint, srcRect, srcUnit, callback, IntPtr.Zero);
+
+ ///
+ /// Sends the records in a selected rectangle from a Metafile, one at a time, to a callback
+ /// method for display at a specified point.
+ ///
+ public void EnumerateMetafile(object metafile, Point destPoint, Rectangle srcRect, GraphicsUnit srcUnit, object callback, IntPtr callbackData)
+ => EnumerateMetafile(metafile, destPoint, srcRect, srcUnit, callback, callbackData, null);
+
+ ///
+ /// Sends the records of a selected rectangle from a Metafile, one at a time, to a callback
+ /// method for display in a specified rectangle.
+ ///
+ public void EnumerateMetafile(object metafile, RectangleF destRect, RectangleF srcRect, GraphicsUnit srcUnit, object callback)
+ => EnumerateMetafile(metafile, destRect, srcRect, srcUnit, callback, IntPtr.Zero);
+
+ ///
+ /// Sends the records of a selected rectangle from a Metafile, one at a time, to a callback
+ /// method for display in a specified rectangle.
+ ///
+ public void EnumerateMetafile(object metafile, RectangleF destRect, RectangleF srcRect, GraphicsUnit srcUnit, object callback, IntPtr callbackData)
+ => EnumerateMetafile(metafile, destRect, srcRect, srcUnit, callback, callbackData, null);
+
+ ///
+ /// Sends the records of a selected rectangle from a Metafile, one at a time, to a callback
+ /// method for display in a specified rectangle.
+ ///
+ public void EnumerateMetafile(object metafile, Rectangle destRect, Rectangle srcRect, GraphicsUnit srcUnit, object callback)
+ => EnumerateMetafile(metafile, destRect, srcRect, srcUnit, callback, IntPtr.Zero);
+
+ ///
+ /// Sends the records of a selected rectangle from a Metafile, one at a time, to a callback
+ /// method for display in a specified rectangle.
+ ///
+ public void EnumerateMetafile(object metafile, Rectangle destRect, Rectangle srcRect, GraphicsUnit srcUnit, object callback, IntPtr callbackData)
+ => EnumerateMetafile(metafile, destRect, srcRect, srcUnit, callback, callbackData, null);
+
+ ///
+ /// Sends the records in a selected rectangle from a Metafile, one at a time, to a callback
+ /// method for display in a specified parallelogram.
+ ///
+ public void EnumerateMetafile(object metafile, PointF[] destPoints, RectangleF srcRect, GraphicsUnit srcUnit, object callback)
+ => EnumerateMetafile(metafile, destPoints, srcRect, srcUnit, callback, IntPtr.Zero);
+
+ ///
+ /// Sends the records in a selected rectangle from a Metafile, one at a time, to a callback
+ /// method for display in a specified parallelogram.
+ ///
+ public void EnumerateMetafile(object metafile, PointF[] destPoints, RectangleF srcRect, GraphicsUnit srcUnit, object callback, IntPtr callbackData)
+ => EnumerateMetafile(metafile, destPoints, srcRect, srcUnit, callback, callbackData, null);
+
+ ///
+ /// Sends the records in a selected rectangle from a Metafile, one at a time, to a callback
+ /// method for display in a specified parallelogram.
+ ///
+ public void EnumerateMetafile(object metafile, Point[] destPoints, Rectangle srcRect, GraphicsUnit srcUnit, object callback)
+ => EnumerateMetafile(metafile, destPoints, srcRect, srcUnit, callback, IntPtr.Zero);
+
+ ///
+ /// Sends the records in a selected rectangle from a Metafile, one at a time, to a callback
+ /// method for display in a specified parallelogram.
+ ///
+ public void EnumerateMetafile(object metafile, Point[] destPoints, Rectangle srcRect, GraphicsUnit srcUnit, object callback, IntPtr callbackData)
+ => EnumerateMetafile(metafile, destPoints, srcRect, srcUnit, callback, callbackData, null);
+
+ ///
+ /// Sends the records in a selected rectangle from a Metafile, one at a time, to a callback
+ /// method for display at a specified point using specified image attributes.
+ ///
+ public void EnumerateMetafile(object metafile, PointF destPoint, object callback, IntPtr callbackData, object imageAttr)
+ => EnumerateMetafile(metafile, new[] { destPoint }, callback, callbackData, imageAttr);
+
+ ///
+ /// Sends the records in a selected rectangle from a Metafile, one at a time, to a callback
+ /// method for display at a specified point using specified image attributes.
+ ///
+ public void EnumerateMetafile(object metafile, Point destPoint, object callback, IntPtr callbackData, object imageAttr)
+ => EnumerateMetafile(metafile, new[] { destPoint }, callback, callbackData, imageAttr);
+
+ ///
+ /// Sends the records of a selected rectangle from a Metafile, one at a time, to a callback
+ /// method for display in a specified rectangle using specified image attributes.
+ ///
+ public void EnumerateMetafile(object metafile, RectangleF destRect, object callback, IntPtr callbackData, object imageAttr)
+ => EnumerateMetafile(metafile, destRect.Points, callback, callbackData, imageAttr);
+
+ ///
+ /// Sends the records of a selected rectangle from a Metafile, one at a time, to a callback
+ /// method for display in a specified rectangle using specified image attributes.
+ ///
+ public void EnumerateMetafile(object metafile, Rectangle destRect, object callback, IntPtr callbackData, object imageAttr)
+ => EnumerateMetafile(metafile, destRect.Points, callback, callbackData, imageAttr);
+
+ ///
+ /// Sends the records in a selected rectangle from a Metafile, one at a time, to a callback
+ /// method for display in a specified parallelogram using specified image attributes.
+ ///
+ public void EnumerateMetafile(object metafile, PointF[] destPoints, object callback, IntPtr callbackData, object imageAttr)
+ => EnumerateMetafile(metafile, destPoints, RectangleF.Empty, GraphicsUnit.Pixel, callback, callbackData, imageAttr);
+
+ ///
+ /// Sends the records in a selected rectangle from a Metafile, one at a time, to a callback
+ /// method for display in a specified parallelogram using specified image attributes.
+ ///
+ public void EnumerateMetafile(object metafile, Point[] destPoints, object callback, IntPtr callbackData, object imageAttr)
+ => EnumerateMetafile(metafile, destPoints, Rectangle.Empty, GraphicsUnit.Pixel, callback, callbackData, imageAttr);
+
+ ///
+ /// Sends the records in a selected rectangle from a Metafile, one at a time, to a callback
+ /// method for display at a specified point using specified image attributes.
+ ///
+ public void EnumerateMetafile(object metafile, PointF destPoint, RectangleF srcRect, GraphicsUnit srcUnit, object callback, IntPtr callbackData, object imageAttr)
+ => EnumerateMetafile(metafile, new[] { destPoint }, srcRect, srcUnit, callback, callbackData, imageAttr);
+
+ ///
+ /// Sends the records in a selected rectangle from a Metafile, one at a time, to a callback
+ /// method for display at a specified point using specified image attributes.
+ ///
+ public void EnumerateMetafile(object metafile, Point destPoint, Rectangle srcRect, GraphicsUnit srcUnit, object callback, IntPtr callbackData, object imageAttr)
+ => EnumerateMetafile(metafile, new[] { destPoint }, srcRect, srcUnit, callback, callbackData, imageAttr);
+
+ ///
+ /// Sends the records of a selected rectangle from a Metafile, one at a time, to a callback
+ /// method for display in a specified rectangle using specified image attributes.
+ ///
+ public void EnumerateMetafile(object metafile, RectangleF destRect, RectangleF srcRect, GraphicsUnit srcUnit, object callback, IntPtr callbackData, object imageAttr)
+ => EnumerateMetafile(metafile, destRect.Points, srcRect, srcUnit, callback, callbackData, imageAttr);
+
+ ///
+ /// Sends the records of a selected rectangle from a Metafile, one at a time, to a callback
+ /// method for display in a specified rectangle using specified image attributes.
+ ///
+ public void EnumerateMetafile(object metafile, Rectangle destRect, Rectangle srcRect, GraphicsUnit srcUnit, object callback, IntPtr callbackData, object imageAttr)
+ => EnumerateMetafile(metafile, destRect.Points, srcRect, srcUnit, callback, callbackData, imageAttr);
+
+ ///
+ /// Sends the records in a selected rectangle from a Metafile, one at a time, to a callback
+ /// method for display in a specified parallelogram using specified image attributes.
+ ///
+ public void EnumerateMetafile(object metafile, PointF[] destPoints, RectangleF srcRect, GraphicsUnit srcUnit, object callback, IntPtr callbackData, object imageAttr)
+ => throw new NotImplementedException(); // TODO: Implement Metafile, EnumerateMetafileProc, ImageAttributes classes
+
+ ///
+ /// Sends the records in a selected rectangle from a Metafile, one at a time, to a callback
+ /// method for display in a specified parallelogram using specified image attributes.
+ ///
+ public void EnumerateMetafile(object metafile, Point[] destPoints, Rectangle srcRect, GraphicsUnit srcUnit, object callback, IntPtr callbackData, object imageAttr)
+ => EnumerateMetafile(metafile, Array.ConvertAll(destPoints, point => new PointF(point.m_point)), new RectangleF(srcRect.m_rect), srcUnit, callback, callbackData, imageAttr);
+
+ ///
+ /// Updates the clip region of this to exclude the area specified
+ /// by a .
+ ///
+ public void ExcludeClip(Region region)
+ => SetClip(region, CombineMode.Exclude);
+
+ ///
+ /// Updates the clip region of this to exclude the area specified
+ /// by a structure.
+ ///
+ public void ExcludeClip(Rectangle rect)
+ => ExcludeClip(new Region(rect));
+
+ ///
+ /// Fills the interior of a closed cardinal spline curve defined by an array
+ /// of structures using the specified fill mode and tension
+ ///
+ public void FillClosedCurve(Brush brush, PointF[] points, FillMode fillMode = FillMode.Alternate, float tension = 0.5f)
+ {
+ using var path = GetCurvePath(points, fillMode, tension, true);
+ FillPath(brush, path);
+ }
+
+ ///
+ /// Fills the interior of a closed cardinal spline curve defined by an array
+ /// of structures using the specified fill mode and tension
+ ///
+ public void FillClosedCurve(Brush brush, Point[] points, FillMode fillMode = FillMode.Alternate, float tension = 0.5f)
+ => FillClosedCurve(brush, Array.ConvertAll(points, point => new PointF(point.m_point)));
+
+ ///
+ /// Fills the interior of an ellipse defined by a bounding rectangle specified
+ /// by a pair of coordinates, a width, and a height.
+ ///
+ public void FillEllipse(Brush brush, float x, float y, float width, float height)
+ => FillEllipse(brush, new RectangleF(x, y, width, height));
+
+ ///
+ /// Fills the interior of an ellipse defined by a bounding rectangle specified
+ /// by a pair of coordinates, a width, and a height.
+ ///
+ public void FillEllipse(Brush brush, int x, int y, int width, int height)
+ => FillEllipse(brush, new Rectangle(x, y, width, height));
+
+ ///
+ /// Fills the interior of an ellipse defined by a bounding
+ /// specified by a Rectangle structure.
+ ///
+ public void FillEllipse(Brush brush, RectangleF rect)
+ {
+ using var path = GetEllipsePath(rect);
+ FillPath(brush, path);
+ }
+
+ ///
+ /// Fills the interior of an ellipse defined by a bounding
+ /// specified by a Rectangle structure.
+ ///
+ public void FillEllipse(Brush brush, Rectangle rect)
+ => FillEllipse(brush, new RectangleF(rect.m_rect));
+
+ ///
+ /// Fills the interior of a .
+ ///
+ public void FillPath(Brush brush, GraphicsPath path)
+ => PaintPath(path.m_path, brush.m_paint);
+
+ ///
+ /// Fills the interior of a pie section defined by an ellipse specified by
+ /// a structure and two radial lines.
+ ///
+ public void FillPie(Brush brush, RectangleF oval, float startAngle, float sweepAngle)
+ {
+ using var path = GetPiePath(oval, startAngle, sweepAngle);
+ FillPath(brush, path);
+ }
+
+ ///
+ /// Fills the interior of a pie section defined by an ellipse specified by
+ /// a structure and two radial lines.
+ ///
+ public void FillPie(Brush brush, Rectangle oval, float startAngle, float sweepAngle)
+ => FillPie(brush, new RectangleF(oval.m_rect), startAngle, sweepAngle);
+
+ ///
+ /// Fills the interior of a pie section defined by an ellipse specified by a
+ /// pair of coordinates, a width, a height, and two radial lines.
+ ///
+ public void FillPie(Brush brush, float x, float y, float width, float height, float startAngle, float sweepAngle)
+ => FillPie(brush, new RectangleF(x, y, width, height), startAngle, sweepAngle);
+
+ ///
+ /// Fills the interior of a pie section defined by an ellipse specified by a
+ /// pair of coordinates, a width, a height, and two radial lines.
+ ///
+ public void FillPie(Brush brush, int x, int y, int width, int height, float startAngle, float sweepAngle)
+ => FillPie(brush, new Rectangle(x, y, width, height), startAngle, sweepAngle);
+
+ ///
+ /// Fills the interior of a polygon defined by an array of structures
+ /// and optionally using the specified fill mode.
+ ///
+ public void FillPolygon(Brush brush, PointF[] points, FillMode fillMode = FillMode.Alternate)
+ {
+ using var path = GetPolygonPath(points, fillMode);
+ FillPath(brush, path);
+ }
+
+ ///
+ /// Fills the interior of a polygon defined by an array of structures
+ /// and optionally using the specified fill mode.
+ ///
+ public void FillPolygon(Brush brush, Point[] points, FillMode fillMode = FillMode.Alternate)
+ => FillPolygon(brush, Array.ConvertAll(points, point => new PointF(point.m_point)), fillMode);
+
+ ///
+ /// Fills the interior of a rectangle specified by a structure.
+ ///
+ public void FillRectangle(Brush brush, RectangleF rect)
+ {
+ using var path = GetRectanglePath(rect);
+ FillPath(brush, path);
+ }
+
+ ///
+ /// Fills the interior of a rectangle specified by a structure.
+ ///
+ public void FillRectangle(Brush brush, Rectangle rect)
+ => FillRectangle(brush, new RectangleF(rect.m_rect));
+
+ ///
+ /// Fills the interior of a rectangle specified by a pair of coordinates, a width, and a height.
+ ///
+ public void FillRectangle(Brush brush, float x, float y, float width, float height)
+ => FillRectangle(brush, new RectangleF(x, y, width, height));
+
+ ///
+ /// Fills the interior of a rectangle specified by a pair of coordinates, a width, and a height.
+ ///
+ public void FillRectangle(Brush brush, int x, int y, int width, int height)
+ => FillRectangle(brush, new Rectangle(x, y, width, height));
+
+ ///
+ /// Fills the interiors of a series of rectangles specified by structures.
+ ///
+ public void FillRectangles(Brush brush, params RectangleF[] rects)
+ => Array.ForEach(rects, rect => FillRectangle(brush, rect));
+
+ ///
+ /// Fills the interiors of a series of rectangles specified by structures.
+ ///
+ public void FillRectangles(Brush brush, params Rectangle[] rects)
+ => FillRectangles(brush, Array.ConvertAll(rects, rect => new RectangleF(rect.m_rect)));
+
+ ///
+ /// Fills the interior of a .
+ ///
+ public void FillRegion(Brush brush, Region region)
+ {
+ using var boundaries = region.m_region.GetBoundaryPath();
+ using var path = new GraphicsPath(boundaries);
+ FillPath(brush, path);
+ }
+
+ ///
+ /// Forces execution of all pending graphics operations with the method waiting
+ /// or not waiting, as specified, to return before the operations finish.
+ ///
+ public void Flush(FlushIntention intention = FlushIntention.Flush)
+ {
+ if (intention == FlushIntention.Sync)
+ throw new NotSupportedException($"skia unsupported feature (param: {intention})");
+ m_canvas.Flush();
+ }
+
+ ///
+ /// Combines current Graphics context with all previous contexts.
+ /// When BeginContainer() is called, a copy of the current context is pushed into the GDI+ context stack, it keeps track of the
+ /// absolute clipping and transform but reset the public properties so it looks like a brand new context.
+ /// When Save() is called, a copy of the current context is also pushed in the GDI+ stack but the public clipping and transform
+ /// properties are not reset (cumulative). Consecutive Save context are ignored with the exception of the top one which contains
+ /// all previous information.
+ /// The return value is an object array where the first element contains the cumulative clip region and the second the cumulative
+ /// translate transform matrix.
+ ///
+ public object GetContextInfo()
+ {
+ using var path = new GraphicsPath(m_path);
+
+ using var cumulativeClip = m_canvas.IsClipEmpty ? null : new Region(path);
+ var cumulativeTransform = TransformElements;
+
+ // TODO: keep the context tracking when calling to BeginContainer/Save and EndContainer/Restore
+ return new object[] { cumulativeClip ?? new Region(), new Matrix(cumulativeTransform) };
+ }
+
+ ///
+ /// Gets a handle to the current Windows halftone palette.
+ ///
+ public static IntPtr GetHalftonePalette()
+ => throw new NotSupportedException("skia unsupported feature");
+
+ ///
+ /// Gets the handle to the device context associated with this .
+ ///
+ public IntPtr GetHdc()
+ => throw new NotSupportedException("skia unsupported feature");
+
+ ///
+ /// Gets the nearest color to the specified structure.
+ ///
+ public Color GetNearestColor(Color color)
+ => color; // NOTE: skia does not provides the device color palette to find the nearest color
+
+ ///
+ /// Updates the clip region of this to the intersection
+ /// of the current clip region and the specified .
+ ///
+ public void IntersectClip(Region region)
+ => SetClip(region, CombineMode.Intersect); // TODO: implement Region
+
+ ///
+ /// Updates the clip region of this to the intersection
+ /// of the current clip region and the specified structure.
+ ///
+ public void IntersectClip(RectangleF rect)
+ => IntersectClip(new Region(rect));
+
+ ///
+ /// Updates the clip region of this to the intersection
+ /// of the current clip region and the specified structure.
+ ///
+ public void IntersectClip(Rectangle rect)
+ => IntersectClip(new RectangleF(rect.m_rect));
+
+ ///
+ /// Indicates whether the specified structure is contained
+ /// within the visible clip region of this .
+ ///
+ public bool IsVisible(PointF point)
+ => IsVisible(new RectangleF(point, new SizeF(1, 1)));
+
+ ///
+ /// Indicates whether the specified structure is contained
+ /// within the visible clip region of this .
+ ///
+ public bool IsVisible(Point point)
+ => IsVisible(new PointF(point.X, point.Y));
+
+ ///
+ /// Indicates whether the specified structure is contained
+ /// within the visible clip region of this .
+ ///
+ public bool IsVisible(RectangleF rect)
+ {
+ using var copy = new SKPath(m_path);
+ using var path = new GraphicsPath(copy);
+ using var region = new Region(path);
+ return region.IsVisible(rect);
+ }
+
+ ///
+ /// Indicates whether the specified structure is contained
+ /// within the visible clip region of this .
+ ///
+ public bool IsVisible(Rectangle rect)
+ => IsVisible(new RectangleF(rect.X, rect.Y, rect.Width, rect.Height));
+
+ ///
+ /// Indicates whether the point specified by a pair of coordinates structure is contained
+ /// within the visible clip region of this .
+ ///
+ public bool IsVisible(int x, int y)
+ => IsVisible(new Point(x, y));
+
+ ///
+ /// Indicates whether the point specified by a pair of coordinates structure is contained
+ /// within the visible clip region of this .
+ ///
+ public bool IsVisible(float x, float y)
+ => IsVisible(new PointF(x, y));
+
+ ///
+ /// Indicates whether the specified by a pair of coordinates, a width, and a height is contained
+ /// within the visible clip region of this .
+ ///
+ public bool IsVisible(float x, float y, float width, float height)
+ => IsVisible(new RectangleF(x, y, width, height));
+
+ ///
+ /// Indicates whether the specified by a pair of coordinates, a width, and a height is contained
+ /// within the visible clip region of this .
+ ///
+ public bool IsVisible(int x, int y, int width, int height)
+ => IsVisible(new Rectangle(x, y, width, height));
+
+ ///
+ /// Gets an array of objects, each of which bounds a range of character
+ /// positions within the specified string.
+ ///
+ public Region[] MeasureCharacterRanges(ReadOnlySpan text, Font font, RectangleF layout, StringFormat format)
+ => MeasureCharacterRanges(new string(text.ToArray()), font, layout, format);
+
+ ///
+ /// Gets an array of objects, each of which bounds a range of character
+ /// positions within the specified string.
+ ///
+ public Region[] MeasureCharacterRanges(string text, Font font, RectangleF layout, StringFormat format)
+ {
+ var regions = new List();
+ foreach (var substring in format.ApplyRanges(text))
+ {
+ var bounds = GetStringBounds(substring, font, layout, format);
+ var region = new Region(bounds);
+ regions.Add(region);
+ }
+
+ return regions.ToArray();
+ }
+
+ ///
+ /// Measures the specified string when drawn with the specified in a area,
+ /// formatted with the specified , and the maximum bounding box
+ /// specified by a structure.
+ ///
+ public SizeF MeasureString(string text, Font font, RectangleF layoutArea = default, StringFormat format = null)
+ => MeasureString(text, font, layoutArea.Size, format, out int _, out int _);
+
+ ///
+ /// Measures the specified string when drawn with the specified in a area, formatted
+ /// with the specified , and a maximum layout area specified by
+ /// a structure.
+ ///
+ public SizeF MeasureString(string text, Font font, SizeF layoutArea, StringFormat format = null)
+ => MeasureString(text, font, new RectangleF(0, 0, layoutArea), format);
+
+ ///
+ /// Measures the specified string when drawn with the specified from an origin , formatted
+ /// with the specified , and the upper-left corner speficied by
+ /// a structure.
+ ///
+ public SizeF MeasureString(string text, Font font, PointF origin, StringFormat format = null)
+ => MeasureString(text, font, new RectangleF(origin, 0, 0), format);
+
+ ///
+ /// Measures the specified string when drawn with the specified and certain width area, formatted
+ /// with the specified , and indicating the maximum width of the string.
+ ///
+ public SizeF MeasureString(string text, Font font, int width, StringFormat format = null)
+ => MeasureString(text, font, new SizeF(width, int.MaxValue), format);
+
+ ///
+ /// Measures the specified string when drawn with the specified in a area, formatted
+ /// with the specified , and returning the structure,
+ /// the numbers of characters in the string, and the number of text lines in the string.
+ ///
+ public SizeF MeasureString(string text, Font font, SizeF layoutArea, StringFormat format, out int charsFitted, out int linesFilled)
+ => MeasureStringInternal(new ReadOnlySpan(text.ToArray()), font, new RectangleF(0, 0, layoutArea), format, out charsFitted, out linesFilled);
+
+ ///
+ /// Measures the specified string when drawn with the specified in a area, formatted
+ /// with the specified , and the maximum bounding box
+ /// specified by a structure.
+ ///
+ public SizeF MeasureString(ReadOnlySpan text, Font font, RectangleF layoutArea = default, StringFormat format = null)
+ => MeasureString(new string(text.ToArray()), font, layoutArea, format);
+
+ ///
+ /// Measures the specified string when drawn with the specified in a area, formatted
+ /// with the specified , and a maximum layout area specified by
+ /// a structure.
+ ///
+ public SizeF MeasureString(ReadOnlySpan text, Font font, SizeF layoutArea, StringFormat format = null)
+ => MeasureString(new string (text.ToArray()), font, layoutArea, format);
+
+ ///
+ /// Measures the specified string when drawn with the specified from an origin , formatted
+ /// with the specified , and the upper-left corner speficied by
+ /// a structure.
+ ///
+ public SizeF MeasureString(ReadOnlySpan text, Font font, PointF origin, StringFormat format = null)
+ => MeasureString(new string(text.ToArray()), font, origin, format);
+
+ ///
+ /// Measures the specified string when drawn with the specified and certail width area, formatted
+ /// with the specified , and indicating the maximum width of the string.
+ ///
+ public SizeF MeasureString(ReadOnlySpan text, Font font, int width, StringFormat format = null)
+ => MeasureString(new string(text.ToArray()), font, width, format);
+
+ ///
+ /// Measures the specified string when drawn with the specified in a area, formatted
+ /// with the specified , and returning the structure,
+ /// the numbers of characters in the string, and the number of text lines in the string.
+ ///
+ public SizeF MeasureString(ReadOnlySpan text, Font font, SizeF layoutArea, StringFormat format, out int charsFitted, out int linesFilled)
+ => MeasureString(new string(text.ToArray()), font, layoutArea, format, out charsFitted, out linesFilled);
+
+ ///
+ /// Measures the size of a string when drawn with the specified in a area,
+ /// formatted with the specified , and returning the structure,
+ /// the numbers of characters in the string, and the number of text lines in the string.
+ ///
+ public SizeF MeasureStringInternal(ReadOnlySpan text, Font font, RectangleF layoutArea, StringFormat format, out int charsFitted, out int linesFilled)
+ => MeasureStringInternal(new string(text.ToArray()), font, layoutArea, format, out charsFitted, out linesFilled);
+
+ ///
+ /// Measures the size of a string when drawn with the specified in a area,
+ /// formatted with the specified , and returning the structure,
+ /// the numbers of characters in the string, and the number of text lines in the string.
+ ///
+ public SizeF MeasureStringInternal(string text, Font font, RectangleF layoutArea, StringFormat format, out int charsFitted, out int linesFilled)
+ {
+ var bounds = GetStringBounds(text, font, layoutArea, format);
+ var lines = text.Split(StringFormat.BREAKLINES, StringSplitOptions.None);
+
+ float totalWidth = bounds.Width;
+ float charWidth = totalWidth / lines.Max(line => line.Length);
+ charsFitted = (int)(totalWidth / charWidth);
+
+ float totalHeight = bounds.Height;
+ float lineHeight = totalHeight / lines.Length;
+ linesFilled = (int)(totalHeight / lineHeight);
+
+ return new SizeF(totalWidth, totalHeight);
+ }
+
+ ///
+ /// Multiplies the world transformation of this and
+ /// speciried the in the specified order.
+ ///
+ public void MultiplyTransform(Matrix matrix, MatrixOrder order = MatrixOrder.Prepend)
+ {
+ var result = new Matrix(Transform.m_matrix);
+ result.Multiply(matrix, order);
+ Transform = result;
+ }
+
+ ///
+ /// Releases a device context handle obtained by a previous call to
+ /// the method of this .
+ ///
+ public void ReleaseHdc()
+ => ReleaseHdc(IntPtr.Zero);
+
+ ///
+ /// Releases a device context handle obtained by a previous call to
+ /// the method of this .
+ ///
+ public void ReleaseHdc(IntPtr hdc)
+ => throw new NotSupportedException("skia unsupported feature");
+
+ ///
+ /// Releases a handle to a device context.
+ ///
+ public void ReleaseHdcInternal(IntPtr hdc)
+ => throw new NotSupportedException("skia unsupported feature");
+
+ ///
+ /// Resets the clip region of this to an infinite region.
+ ///
+ public void ResetClip()
+ {
+ m_context = -1;
+
+ // NOTE: apply a full reset without losing the transformation matrix
+ var matrix = m_canvas.TotalMatrix;
+ m_canvas.RestoreToCount(m_context);
+ m_canvas.SetMatrix(matrix);
+
+ ClipRegion.MakeInfinite();
+ SetClip(ClipRegion);
+ }
+
+ ///
+ /// Resets the world transformation matrix of this to
+ /// an the identity matrix.
+ ///
+ public void ResetTransform()
+ => m_canvas.ResetMatrix();
+
+ ///
+ /// Restores the state of this to the state
+ /// represented by a .
+ ///
+ public void Restore(GraphicsState state)
+ => m_canvas.RestoreToCount(state.m_index);
+
+ ///
+ /// Applies the specified rotation to the transformation matrix of this .
+ ///
+ public void RotateTransform(float angle, MatrixOrder order = MatrixOrder.Prepend)
+ {
+ var scaleMatrix = SKMatrix.CreateRotationDegrees(-angle);
+ if (order == MatrixOrder.Append)
+ scaleMatrix = scaleMatrix.PreConcat(m_canvas.TotalMatrix);
+ m_canvas.Concat(ref scaleMatrix);
+ }
+
+ ///
+ /// Saves the current state of this and
+ /// identifies the saved state with a .
+ ///
+ public GraphicsState Save()
+ => new(m_canvas.SaveLayer());
+
+ ///
+ /// Applies the specified scaling operation to the transformation matrix of
+ /// this by prepending it to the object's transformation matrix.
+ ///
+ public void ScaleTransform(float sx, float sy, MatrixOrder order = MatrixOrder.Prepend)
+ {
+ var scaleMatrix = SKMatrix.CreateScale(sx, sy);
+ if (order == MatrixOrder.Append)
+ scaleMatrix = scaleMatrix.PreConcat(m_canvas.TotalMatrix);
+ m_canvas.Concat(ref scaleMatrix);
+ }
+
+ ///
+ /// Sets the clipping region of this to the result
+ /// of the specified operation combining the current clip region and the
+ /// specified .
+ ///
+ public void SetClip(Region region, CombineMode combineMode = CombineMode.Replace)
+ {
+ switch (combineMode)
+ {
+ case CombineMode.Replace:
+ ClipRegion = region;
+ break;
+
+ case CombineMode.Union:
+ ClipRegion.Union(region);
+ break;
+
+ case CombineMode.Intersect:
+ ClipRegion.Intersect(region);
+ break;
+
+ case CombineMode.Exclude:
+ ClipRegion.Exclude(region);
+ break;
+
+ case CombineMode.Complement:
+ ClipRegion.Complement(region);
+ break;
+
+ case CombineMode.Xor:
+ ClipRegion.Xor(region);
+ break;
+
+ default:
+ throw new ArgumentException($"{combineMode} value is not supported", nameof(combineMode));
+ }
+ m_context = m_canvas.Save();
+ m_canvas.ClipRegion(ClipRegion.m_region);
+ m_canvas.Clear(ClipColor.m_color);
+ }
+
+ ///
+ /// Sets the clipping region of this to the result
+ /// of the specified operation combining the current clip region and the
+ /// specified structure.
+ ///
+ public void SetClip(Rectangle rect, CombineMode combineMode = CombineMode.Replace)
+ => SetClip(new Region(rect), combineMode);
+
+ ///
+ /// Sets the clipping region of this to the result
+ /// of the specified combining operation of the current region
+ /// and the Clip property of the specified .
+ ///
+ public void SetClip(Graphics g, CombineMode combineMode = CombineMode.Replace)
+ => SetClip(g.Clip, combineMode);
+
+ ///
+ /// Sets the clipping region of this to the result
+ /// of the specified operation combining the current clip region and the
+ /// specified .
+ ///
+ public void SetClip(GraphicsPath path, CombineMode combineMode = CombineMode.Replace)
+ => SetClip(new Region(path), combineMode);
+
+ ///
+ /// Transforms an array of points from one coordinate space to another using
+ /// the current world and page transformations of this .
+ ///
+ public void TransformPoints(CoordinateSpace destination, CoordinateSpace source, PointF[] points)
+ => TransformPoints(destination, source, points, p => p.m_point, p => new(p.X, p.Y));
+
+ ///
+ /// Transforms an array of points from one coordinate space to another using
+ /// the current world and page transformations of this .
+ ///
+ public void TransformPoints(CoordinateSpace destination, CoordinateSpace source, Point[] points)
+ => TransformPoints(destination, source, points, p => p.m_point, p => new((int)p.X, (int)p.Y));
+
+ ///
+ /// Translates the clipping region of this by specified
+ /// amounts in the horizontal and vertical directions.
+ ///
+ public void TranslateClip(float dx, float dy)
+ {
+ // NOTE: restore without losing the transformation matrix
+ var matrix = m_canvas.TotalMatrix;
+ m_canvas.RestoreToCount(m_context);
+ m_canvas.SetMatrix(matrix);
+
+ Clip.Translate(dx, dy);
+ m_canvas.ClipRegion(Clip.m_region);
+ m_canvas.Clear(ClipColor.m_color);
+ }
+
+ ///
+ /// Changes the origin of the coordinate system by applying the specified translation to the
+ /// transformation matrix of this in the specified order.
+ ///
+ public void TranslateTransform(float dx, float dy, MatrixOrder order = MatrixOrder.Prepend)
+ {
+ var scaleMatrix = SKMatrix.CreateTranslation(dx, dy);
+ if (order == MatrixOrder.Append)
+ scaleMatrix = scaleMatrix.PreConcat(m_canvas.TotalMatrix);
+ m_canvas.Concat(ref scaleMatrix);
+ }
+
+ #endregion
+
+
+ #region Utitlies
+
+ internal static float GetFactor(float dpi, GraphicsUnit sourceUnit, GraphicsUnit targetUnit)
+ {
+ float sourceFactor = GetPointFactor(sourceUnit, dpi);
+ float targetFactor = GetPointFactor(targetUnit, dpi);
+ return sourceFactor / targetFactor;
+
+ static float GetPointFactor(GraphicsUnit unit, float dpi) => unit switch
+ {
+ GraphicsUnit.World => throw new NotSupportedException("World unit conversion is not supported."),
+ GraphicsUnit.Display => 72 / dpi, // Assuming display unit is pixels
+ GraphicsUnit.Pixel => 72 / dpi, // 1 pixel = 72 points per inch / Dots Per Inch
+ GraphicsUnit.Point => 1, // Already in points
+ GraphicsUnit.Inch => 72, // 1 inch = 72 points
+ GraphicsUnit.Document => 72 / 300f, // 1 document unit = 1/300 inch
+ GraphicsUnit.Millimeter => 72 / 25.4f, // 1 millimeter = 1/25.4 inch
+ _ => throw new ArgumentException("Invalid GraphicsUnit")
+ };
+ }
+
+
+ #endregion
+
+
+ #region Helpers
+
+ private Color ClipColor { get; set; }
+
+ private Region ClipRegion { get; set; }
+
+ private void PaintPath(SKPath path, SKPaint paint)
+ {
+ using var render = paint.Clone();
+ render.BlendMode = CompositingMode switch
+ {
+ CompositingMode.SourceOver => SKBlendMode.SrcOver,
+ CompositingMode.SourceCopy => SKBlendMode.Src,
+ _ => throw new NotImplementedException()
+ };
+ render.FilterQuality = InterpolationMode switch
+ {
+ InterpolationMode.NearestNeighbor
+ => SKFilterQuality.None,
+ InterpolationMode.Low
+ => SKFilterQuality.Low,
+ InterpolationMode.Default or
+ InterpolationMode.Bilinear
+ => SKFilterQuality.Medium,
+ InterpolationMode.High or
+ InterpolationMode.Bicubic or
+ InterpolationMode.HighQualityBilinear or
+ InterpolationMode.HighQualityBicubic
+ => SKFilterQuality.High,
+ _ => throw new NotImplementedException()
+ };
+ render.IsAntialias = SmoothingMode == SmoothingMode.AntiAlias;
+
+ if (render.IsDither)
+ {
+ var translation = SKMatrix.CreateTranslation(RenderingOrigin.X, RenderingOrigin.Y);
+ render.Shader = render.Shader.WithLocalMatrix(translation);
+ }
+
+ m_canvas.DrawPath(path, render);
+ m_path.AddPath(path); // used by IsVisible method
+ }
+
+ private static GraphicsPath GetCurvePath(PointF[] points, FillMode fillMode, float tension, bool closed)
+ {
+ if (points.Length < 3)
+ throw new ArgumentException("invalid number of points for drawing a closed curve (at least 3)");
+
+ var path = new GraphicsPath(fillMode);
+ if (closed)
+ path.AddClosedCurve(points, tension);
+ else
+ path.AddCurve(points, tension);
+
+ return path;
+ }
+
+ private static GraphicsPath GetEllipsePath(RectangleF rect)
+ {
+ var path = new GraphicsPath();
+ path.AddEllipse(rect);
+ return path;
+ }
+
+ private static GraphicsPath GetPiePath(RectangleF rect, float startAngle, float sweepAngle)
+ {
+ var path = new GraphicsPath();
+ path.AddPie(rect, startAngle, sweepAngle);
+ return path;
+ }
+
+ private static GraphicsPath GetPolygonPath(PointF[] points, FillMode fillMode)
+ {
+ var path = new GraphicsPath(fillMode);
+ path.AddPolygon(points);
+ return path;
+ }
+
+ private static GraphicsPath GetRectanglePath(RectangleF rect)
+ {
+ var path = new GraphicsPath();
+ path.AddRectangle(rect);
+ return path;
+ }
+
+ private RectangleF GetStringBounds(string text, Font font, RectangleF layout, StringFormat format)
+ {
+ /* NOTE: Skia's MeasureText is not used because of StringFormat rendering, that's why
+ * we use GraphicsPath that already supports StringFormat class definition
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+ if (text == null) throw new ArgumentNullException(nameof(text));
+ if (font == null) throw new ArgumentNullException(nameof(font));
+
+ float emSize = DpiY * GetFactor(font.Size, font.Unit, GraphicsUnit.Pixel);
+
+ using var path = new GraphicsPath();
+ path.AddString(text, font.FontFamily, (int)font.Style, emSize, layout, format);
+
+ var bounds = path.GetBounds();
+
+ // NOTE: the Y value is always 0 in System.Drawing, so that distance is added to height twice (for top and bottom)
+ bounds.Height += 2 * bounds.Y;
+ bounds.Y = 0;
+
+ return bounds;
+ }
+
+ private void TransformPoints(CoordinateSpace destination, CoordinateSpace source, T[] points, Func getPoint, Func newPoint)
+ {
+ if (source == destination)
+ return;
+
+ void ApplyTransform(SKMatrix matrix)
+ {
+ for (int i = 0; i < points.Length; i++)
+ {
+ var srcPoint = getPoint(points[i]);
+ var dstPoint = matrix.MapPoint(srcPoint);
+ points[i] = newPoint(dstPoint);
+ }
+ }
+
+ // get factors according to page unot and scale
+ var factorX = GetFactor(DpiX, PageUnit, GraphicsUnit.Pixel) * PageScale;
+ var factorY = GetFactor(DpiY, PageUnit, GraphicsUnit.Pixel) * PageScale;
+
+ var factorMatrix = SKMatrix.CreateScale(factorX, factorY);
+
+ // get the destination and source matrices
+ var dstTransMatrix = new SKMatrix(m_canvas.TotalMatrix.Values);
+ var dstScaleMatrix = SKMatrix.Concat(dstTransMatrix, factorMatrix);
+
+ var srcTransMatrix = dstTransMatrix.Invert();
+ var srcScaleMatrix = dstScaleMatrix.Invert();
+
+ // apply transform based on source
+ switch (source)
+ {
+ case CoordinateSpace.World:
+ break;
+
+ case CoordinateSpace.Page:
+ ApplyTransform(srcTransMatrix);
+ break;
+
+ case CoordinateSpace.Device:
+ ApplyTransform(srcScaleMatrix);
+ break;
+
+ default:
+ throw new ArgumentException($"{source} coordinate space is not supported.", nameof(source));
+ }
+
+ // apply transform based on destination
+ switch (destination)
+ {
+ case CoordinateSpace.World:
+ break;
+
+ case CoordinateSpace.Page:
+ ApplyTransform(dstTransMatrix);
+ break;
+
+ case CoordinateSpace.Device:
+ ApplyTransform(dstScaleMatrix);
+ break;
+
+ default:
+ throw new ArgumentException($"{destination} coordinate space is not supported.", nameof(destination));
+ }
+ }
+
+ #endregion
+}
diff --git a/src/Common/Icon.cs b/src/Common/Icon.cs
index 78b371f..0d2631f 100644
--- a/src/Common/Icon.cs
+++ b/src/Common/Icon.cs
@@ -36,7 +36,7 @@ private Icon(Bitmap bitmap, List entries, float width, float height)
}
///
- /// Initializes a new instance of the class from a instance.
+ /// Initializes a new instance of the class from a instance.
///
public Icon(Bitmap bitmap)
: this(bitmap, new() { new IconEntry { Width = (byte)bitmap.Width, Height = (byte)bitmap.Height } }, -1, -1) { }
@@ -256,7 +256,7 @@ private static List ReadIco(Stream stream)
{
Reserved = reader.ReadUInt16(),
Type = reader.ReadUInt16(), // Type (1 for icon, 2 for cursor)
- Count = reader.ReadUInt16() // Number of images
+ Count = reader.ReadUInt16() // Number of images
};
// Read ICONDIRENTRY structures
diff --git a/src/Common/KnownColor.cs b/src/Common/KnownColor.cs
index 50b9151..987c56b 100644
--- a/src/Common/KnownColor.cs
+++ b/src/Common/KnownColor.cs
@@ -7,189 +7,189 @@ namespace GeneXus.Drawing;
**/
public enum KnownColor
- {
- // 0 - reserved for "not a known color"
+ {
+ // 0 - reserved for "not a known color"
- // "System" colors, Part 1
- ActiveBorder = 1,
- ActiveCaption,
- ActiveCaptionText,
- AppWorkspace,
- Control,
- ControlDark,
- ControlDarkDark,
- ControlLight,
- ControlLightLight,
- ControlText,
- Desktop,
- GrayText,
- Highlight,
- HighlightText,
- HotTrack,
- InactiveBorder,
- InactiveCaption,
- InactiveCaptionText,
- Info,
- InfoText,
- Menu,
- MenuText,
- ScrollBar,
- Window,
- WindowFrame,
- WindowText,
+ // "System" colors, Part 1
+ ActiveBorder = 1,
+ ActiveCaption,
+ ActiveCaptionText,
+ AppWorkspace,
+ Control,
+ ControlDark,
+ ControlDarkDark,
+ ControlLight,
+ ControlLightLight,
+ ControlText,
+ Desktop,
+ GrayText,
+ Highlight,
+ HighlightText,
+ HotTrack,
+ InactiveBorder,
+ InactiveCaption,
+ InactiveCaptionText,
+ Info,
+ InfoText,
+ Menu,
+ MenuText,
+ ScrollBar,
+ Window,
+ WindowFrame,
+ WindowText,
- // "Web" Colors, Part 1
- Transparent,
- AliceBlue,
- AntiqueWhite,
- Aqua,
- Aquamarine,
- Azure,
- Beige,
- Bisque,
- Black,
- BlanchedAlmond,
- Blue,
- BlueViolet,
- Brown,
- BurlyWood,
- CadetBlue,
- Chartreuse,
- Chocolate,
- Coral,
- CornflowerBlue,
- Cornsilk,
- Crimson,
- Cyan,
- DarkBlue,
- DarkCyan,
- DarkGoldenrod,
- DarkGray,
- DarkGreen,
- DarkKhaki,
- DarkMagenta,
- DarkOliveGreen,
- DarkOrange,
- DarkOrchid,
- DarkRed,
- DarkSalmon,
- DarkSeaGreen,
- DarkSlateBlue,
- DarkSlateGray,
- DarkTurquoise,
- DarkViolet,
- DeepPink,
- DeepSkyBlue,
- DimGray,
- DodgerBlue,
- Firebrick,
- FloralWhite,
- ForestGreen,
- Fuchsia,
- Gainsboro,
- GhostWhite,
- Gold,
- Goldenrod,
- Gray,
- Green,
- GreenYellow,
- Honeydew,
- HotPink,
- IndianRed,
- Indigo,
- Ivory,
- Khaki,
- Lavender,
- LavenderBlush,
- LawnGreen,
- LemonChiffon,
- LightBlue,
- LightCoral,
- LightCyan,
- LightGoldenrodYellow,
- LightGray,
- LightGreen,
- LightPink,
- LightSalmon,
- LightSeaGreen,
- LightSkyBlue,
- LightSlateGray,
- LightSteelBlue,
- LightYellow,
- Lime,
- LimeGreen,
- Linen,
- Magenta,
- Maroon,
- MediumAquamarine,
- MediumBlue,
- MediumOrchid,
- MediumPurple,
- MediumSeaGreen,
- MediumSlateBlue,
- MediumSpringGreen,
- MediumTurquoise,
- MediumVioletRed,
- MidnightBlue,
- MintCream,
- MistyRose,
- Moccasin,
- NavajoWhite,
- Navy,
- OldLace,
- Olive,
- OliveDrab,
- Orange,
- OrangeRed,
- Orchid,
- PaleGoldenrod,
- PaleGreen,
- PaleTurquoise,
- PaleVioletRed,
- PapayaWhip,
- PeachPuff,
- Peru,
- Pink,
- Plum,
- PowderBlue,
- Purple,
- Red,
- RosyBrown,
- RoyalBlue,
- SaddleBrown,
- Salmon,
- SandyBrown,
- SeaGreen,
- SeaShell,
- Sienna,
- Silver,
- SkyBlue,
- SlateBlue,
- SlateGray,
- Snow,
- SpringGreen,
- SteelBlue,
- Tan,
- Teal,
- Thistle,
- Tomato,
- Turquoise,
- Violet,
- Wheat,
- White,
- WhiteSmoke,
- Yellow,
- YellowGreen,
+ // "Web" Colors, Part 1
+ Transparent,
+ AliceBlue,
+ AntiqueWhite,
+ Aqua,
+ Aquamarine,
+ Azure,
+ Beige,
+ Bisque,
+ Black,
+ BlanchedAlmond,
+ Blue,
+ BlueViolet,
+ Brown,
+ BurlyWood,
+ CadetBlue,
+ Chartreuse,
+ Chocolate,
+ Coral,
+ CornflowerBlue,
+ Cornsilk,
+ Crimson,
+ Cyan,
+ DarkBlue,
+ DarkCyan,
+ DarkGoldenrod,
+ DarkGray,
+ DarkGreen,
+ DarkKhaki,
+ DarkMagenta,
+ DarkOliveGreen,
+ DarkOrange,
+ DarkOrchid,
+ DarkRed,
+ DarkSalmon,
+ DarkSeaGreen,
+ DarkSlateBlue,
+ DarkSlateGray,
+ DarkTurquoise,
+ DarkViolet,
+ DeepPink,
+ DeepSkyBlue,
+ DimGray,
+ DodgerBlue,
+ Firebrick,
+ FloralWhite,
+ ForestGreen,
+ Fuchsia,
+ Gainsboro,
+ GhostWhite,
+ Gold,
+ Goldenrod,
+ Gray,
+ Green,
+ GreenYellow,
+ Honeydew,
+ HotPink,
+ IndianRed,
+ Indigo,
+ Ivory,
+ Khaki,
+ Lavender,
+ LavenderBlush,
+ LawnGreen,
+ LemonChiffon,
+ LightBlue,
+ LightCoral,
+ LightCyan,
+ LightGoldenrodYellow,
+ LightGray,
+ LightGreen,
+ LightPink,
+ LightSalmon,
+ LightSeaGreen,
+ LightSkyBlue,
+ LightSlateGray,
+ LightSteelBlue,
+ LightYellow,
+ Lime,
+ LimeGreen,
+ Linen,
+ Magenta,
+ Maroon,
+ MediumAquamarine,
+ MediumBlue,
+ MediumOrchid,
+ MediumPurple,
+ MediumSeaGreen,
+ MediumSlateBlue,
+ MediumSpringGreen,
+ MediumTurquoise,
+ MediumVioletRed,
+ MidnightBlue,
+ MintCream,
+ MistyRose,
+ Moccasin,
+ NavajoWhite,
+ Navy,
+ OldLace,
+ Olive,
+ OliveDrab,
+ Orange,
+ OrangeRed,
+ Orchid,
+ PaleGoldenrod,
+ PaleGreen,
+ PaleTurquoise,
+ PaleVioletRed,
+ PapayaWhip,
+ PeachPuff,
+ Peru,
+ Pink,
+ Plum,
+ PowderBlue,
+ Purple,
+ Red,
+ RosyBrown,
+ RoyalBlue,
+ SaddleBrown,
+ Salmon,
+ SandyBrown,
+ SeaGreen,
+ SeaShell,
+ Sienna,
+ Silver,
+ SkyBlue,
+ SlateBlue,
+ SlateGray,
+ Snow,
+ SpringGreen,
+ SteelBlue,
+ Tan,
+ Teal,
+ Thistle,
+ Tomato,
+ Turquoise,
+ Violet,
+ Wheat,
+ White,
+ WhiteSmoke,
+ Yellow,
+ YellowGreen,
- // "System" colors, Part 2
- ButtonFace,
- ButtonHighlight,
- ButtonShadow,
- GradientActiveCaption,
- GradientInactiveCaption,
- MenuBar,
- MenuHighlight,
+ // "System" colors, Part 2
+ ButtonFace,
+ ButtonHighlight,
+ ButtonShadow,
+ GradientActiveCaption,
+ GradientInactiveCaption,
+ MenuBar,
+ MenuHighlight,
- // "Web" colors, Part 2
- RebeccaPurple,
- }
\ No newline at end of file
+ // "Web" colors, Part 2
+ RebeccaPurple,
+ }
\ No newline at end of file
diff --git a/src/Common/Pen.cs b/src/Common/Pen.cs
new file mode 100644
index 0000000..bbbe173
--- /dev/null
+++ b/src/Common/Pen.cs
@@ -0,0 +1,317 @@
+
+using System;
+using SkiaSharp;
+using GeneXus.Drawing.Drawing2D;
+
+namespace GeneXus.Drawing;
+
+public class Pen : ICloneable, IDisposable
+{
+ internal SKPaint m_paint;
+ private Brush m_brush;
+
+ internal Pen(SKPaint paint, float width)
+ {
+ m_paint = paint ?? throw new ArgumentNullException(nameof(paint));
+ m_paint.Style = SKPaintStyle.Stroke;
+ m_paint.StrokeWidth = width;
+ m_paint.StrokeMiter = 10;
+ m_paint.TextAlign = SKTextAlign.Center;
+ }
+
+ ///
+ /// Initializes a new instance of the class with the
+ /// specified and .
+ ///
+ public Pen(Color color, float width = 1.0f)
+ : this (new SKPaint() { Color = color.m_color }, width)
+ {
+ m_brush = new SolidBrush(color);
+ }
+
+ ///
+ /// Initializes a new instance of the class with the
+ /// specified and .
+ ///
+ public Pen(Brush brush, float width = 1.0f)
+ : this(brush.m_paint.Clone(), width)
+ {
+ m_brush = brush;
+ }
+
+ ///
+ /// Cleans up resources for this .
+ ///
+ ~Pen() => Dispose(false);
+
+ ///
+ /// Creates a human-readable string that represents this .
+ ///
+ public override string ToString() => $"{GetType().Name}: [{Color}, Width: {Width}]";
+
+
+ #region IDisposable
+
+ ///
+ /// Cleans up resources for this .
+ ///
+ public void Dispose()
+ {
+ GC.SuppressFinalize(this);
+ Dispose(true);
+ }
+
+ protected virtual void Dispose(bool disposing) => m_paint.Dispose();
+
+ #endregion
+
+
+ #region IClonable
+
+ ///
+ /// Creates an exact copy of this .
+ ///
+ public object Clone()
+ => new Pen(Color, Width)
+ {
+ Alignment = Alignment,
+ Brush = Brush switch
+ {
+ SolidBrush sb => sb.Clone() as SolidBrush,
+ HatchBrush hb => hb.Clone() as HatchBrush,
+ TextureBrush tb => tb.Clone() as TextureBrush,
+ PathGradientBrush pgb => pgb.Clone() as PathGradientBrush,
+ LinearGradientBrush lgb => lgb.Clone() as LinearGradientBrush,
+ _ => throw new NotImplementedException($"undefined map to {Brush.GetType().Name}.")
+ },
+ CompoundArray = CompoundArray,
+ DashCap = DashCap,
+ DashOffset = DashOffset,
+ DashPattern = DashPattern,
+ StartCap = StartCap,
+ EndCap = EndCap,
+ LineJoin = LineJoin,
+ MiterLimit = MiterLimit,
+ Transform = new Matrix(Transform.m_matrix)
+ };
+
+ #endregion
+
+
+ #region Operators
+
+ ///
+ /// Creates a with the coordinates of the specified .
+ ///
+ public static explicit operator SKPaint(Pen pen) => pen.m_paint;
+
+ #endregion
+
+
+ #region Properties
+
+ ///
+ /// Gets or sets the alignment for this .
+ ///
+ public PenAlignment Alignment
+ {
+ get => m_paint.TextAlign switch
+ {
+ SKTextAlign.Center => PenAlignment.Center,
+ SKTextAlign.Left => PenAlignment.Left,
+ SKTextAlign.Right => PenAlignment.Right,
+ _ => throw new NotSupportedException($"unsuported value {m_paint.TextAlign}")
+ };
+ set => m_paint.TextAlign = value switch
+ {
+ PenAlignment.Center => SKTextAlign.Center,
+ PenAlignment.Left => SKTextAlign.Left,
+ PenAlignment.Right => SKTextAlign.Right,
+ _ => throw new NotSupportedException($"unsuported value {value}")
+ };
+ }
+
+ ///
+ /// Gets or sets the that determines attributes of this .
+ ///
+ public Brush Brush
+ {
+ get => m_brush;
+ set => m_brush = value;
+ }
+
+ ///
+ /// Gets or sets the color of this .
+ ///
+ public Color Color
+ {
+ get => new(m_paint.Color);
+ set
+ {
+ m_paint.Color = value.m_color;
+ m_brush.m_paint.Color = value.m_color;
+ }
+ }
+
+ ///
+ /// Gets or sets an array of values that specifies a compound . A compound
+ /// pen draws a compound line made up of parallel lines and spaces.
+ ///
+ public float[] CompoundArray
+ {
+ get => m_interval;
+ set
+ {
+ m_interval = value;
+ m_paint.PathEffect = SKPathEffect.CreateDash(m_interval, 0);
+ }
+ }
+
+ private float[] m_interval = Array.Empty();
+
+ ///
+ /// Gets or sets a custom cap to use at the end of lines drawn
+ /// with this .
+ ///
+ public object CustomEndCap // TODO: implement CustomLineCap class
+ {
+ get => throw new NotImplementedException();
+ set => throw new NotImplementedException();
+ }
+
+ ///
+ /// Gets or sets a custom cap to use at the beginning of lines drawn
+ /// with this .
+ ///
+ public object CustomStartCap // TODO: implement CustomLineCap class
+ {
+ get => throw new NotImplementedException();
+ set => throw new NotImplementedException();
+ }
+
+ ///
+ /// Gets or sets the cap style used at the end of the dashes that make
+ /// up dashed lines drawn with this .
+ ///
+ public DashCap DashCap { get; set; } = DashCap.Flat;
+
+ ///
+ /// Gets or sets the distance from the start of a line to the beginning of a dash pattern.
+ ///
+ public float DashOffset { get; set; } = 0.0f;
+
+ ///
+ /// Gets or sets an array of custom dashes and spaces.
+ ///
+ public float[] DashPattern { get; set; } = Array.Empty();
+
+ ///
+ /// Gets the style of lines drawn with this .
+ ///
+ public PenType PenType => m_brush switch
+ {
+ SolidBrush => PenType.SolidColor,
+ TextureBrush => PenType.TextureFill,
+ LinearGradientBrush => PenType.LinearGradient,
+ PathGradientBrush => PenType.PathGradient,
+ HatchBrush => PenType.HatchFill,
+ _ => throw new NotImplementedException($"the {m_brush.GetType().Name} pen type is not implemented.")
+ };
+
+ ///
+ /// Gets or sets the cap style used at the beginning of lines drawn with this .
+ ///
+ public LineCap StartCap { get; set; } = LineCap.Flat;
+
+ ///
+ /// Gets or sets the cap style used at the end of lines drawn with this .
+ ///
+ public LineCap EndCap { get; set; } = LineCap.Flat;
+
+ ///
+ /// Gets or sets the join style for the ends of two consecutive lines drawn with this .
+ ///
+ public LineJoin LineJoin
+ {
+ get => m_paint.StrokeJoin switch
+ {
+ SKStrokeJoin.Bevel => LineJoin.Bevel,
+ SKStrokeJoin.Round => LineJoin.Round,
+ SKStrokeJoin.Miter => LineJoin.Miter,
+ _ => throw new NotImplementedException($"undefined map to {m_paint.StrokeJoin}.")
+ };
+ set => m_paint.StrokeJoin = value switch
+ {
+ LineJoin.Bevel => SKStrokeJoin.Bevel,
+ LineJoin.Round => SKStrokeJoin.Round,
+ LineJoin.Miter => SKStrokeJoin.Miter,
+ _ => throw new ArgumentException($"undefined value {value}.", nameof(value))
+ };
+ }
+
+ ///
+ /// Gets or sets the limit of the thickness of the join on a mitered corner.
+ ///
+ public float MiterLimit
+ {
+ get => m_paint.StrokeMiter;
+ set => m_paint.StrokeMiter = value;
+ }
+
+ ///
+ /// Gets or sets a copy of the geometric transformation for this .
+ ///
+ public Matrix Transform { get; set; } = new Matrix();
+
+ ///
+ /// Gets or sets the width of this .
+ ///
+ public float Width
+ {
+ get => m_paint.StrokeWidth;
+ set => m_paint.StrokeWidth = value;
+ }
+
+ #endregion
+
+
+ #region Mathod
+
+ ///
+ /// Multiplies the transformation matrix for this by the specified .
+ ///
+ public void MultiplyTransform(Matrix matrix)
+ => Transform.Multiply(matrix);
+
+ ///
+ /// Resets the geometric transformation matrix for this to identity.
+ ///
+ public void ResetTransform()
+ => Transform.Reset();
+
+ ///
+ /// Rotates the local geometric transformation by the specified angle.
+ ///
+ public void RotateTransform(float degrees, MatrixOrder order = MatrixOrder.Prepend)
+ => Transform.Rotate(degrees, order);
+
+ ///
+ /// Scales the local geometric transformation by the specified factors.
+ ///
+ public void ScaleTransform(float sx, float sy, MatrixOrder order = MatrixOrder.Prepend)
+ => Transform.Scale(sx, sy, order);
+
+ ///
+ /// Sets the values that determine the style of cap used to end lines drawn by this .
+ ///
+ public void SetLineCap(LineCap startCap, LineCap endCap, DashCap dashCap)
+ => throw new NotImplementedException();
+
+ ///
+ /// Translates the local geometric transformation by the specified dimensions.
+ ///
+ public void TranslateTransform(float dx, float dy, MatrixOrder order = MatrixOrder.Prepend)
+ => Transform.Translate(dx, dy, order);
+
+ #endregion
+}
diff --git a/src/Common/Point.cs b/src/Common/Point.cs
index 05bd35c..c10d8b9 100644
--- a/src/Common/Point.cs
+++ b/src/Common/Point.cs
@@ -8,7 +8,7 @@ public struct Point : IEquatable
{
internal SKPoint m_point;
- private Point(SKPoint point)
+ internal Point(SKPoint point)
{
m_point = point;
}
@@ -59,7 +59,7 @@ public Point(int dw)
///
/// Compares two objects. The result specifies whether the values of the
/// or properties of the two
- /// objects are unequal.
+ /// objects are unequal.
///
public static bool operator !=(Point left, Point right) => left.m_point != right.m_point;
@@ -148,6 +148,21 @@ public int Y
///
public static Point Subtract(Point pt, Size sz) => new(pt.m_point - sz.m_size);
+ ///
+ /// Converts a by performing a ceiling operation on all the coordinates.
+ ///
+ public static Point Ceiling(PointF value) => new(unchecked((int)Math.Ceiling(value.X)), unchecked((int)Math.Ceiling(value.Y)));
+
+ ///
+ /// Converts a by performing a truncate operation on all the coordinates.
+ ///
+ public static Point Truncate(PointF value) => new(unchecked((int)value.X), unchecked((int)value.Y));
+
+ ///
+ /// Converts a by performing a round operation on all the coordinates.
+ ///
+ public static PointF Round(PointF value) => new(unchecked((int)Math.Round(value.X)), unchecked((int)Math.Round(value.Y)));
+
///
/// Translates this by the specified amount.
///
diff --git a/src/Common/PointF.cs b/src/Common/PointF.cs
index b009356..25b52f2 100644
--- a/src/Common/PointF.cs
+++ b/src/Common/PointF.cs
@@ -1,4 +1,5 @@
using System;
+using System.Numerics;
using SkiaSharp;
namespace GeneXus.Drawing;
@@ -8,7 +9,7 @@ public struct PointF : IEquatable
{
internal SKPoint m_point;
- private PointF(SKPoint point)
+ internal PointF(SKPoint point)
{
m_point = point;
}
@@ -31,6 +32,13 @@ public PointF(SizeF sz)
public PointF(int dw)
: this(unchecked((short)((dw >> 0) & 0xFFFF)), unchecked((short)((dw >> 16) & 0xFFFF))) { }
+ ///
+ /// Initializes a new instance of the struct from the specified
+ /// .
+ ///
+ public PointF(Vector2 vector)
+ : this(vector.X, vector.Y) { }
+
///
/// Creates a human-readable string that represents this .
///
@@ -49,6 +57,16 @@ public PointF(int dw)
///
public static explicit operator SizeF(PointF p) => new(p.X, p.Y);
+ ///
+ /// Converts the specified to a .
+ ///
+ public static explicit operator Vector2(PointF point) => point.ToVector2();
+
+ ///
+ /// Converts the specified to a .
+ ///
+ public static explicit operator PointF(Vector2 vector) => new(vector);
+
///
/// Compares two objects. The result specifies whether the values of the
/// and properties of the two
@@ -59,7 +77,7 @@ public PointF(int dw)
///
/// Compares two objects. The result specifies whether the values of the
/// or properties of the two
- /// objects are unequal.
+ /// objects are unequal.
///
public static bool operator !=(PointF left, PointF right) => left.m_point != right.m_point;
@@ -138,6 +156,11 @@ public float Y
#region Methods
+ ///
+ /// Creates a new from this .
+ ///
+ public readonly Vector2 ToVector2() => new(m_point.X, m_point.Y);
+
///
/// Translates a by a given .
///
@@ -148,21 +171,6 @@ public float Y
///
public static PointF Subtract(PointF pt, SizeF sz) => new(pt.m_point - sz.m_size);
- ///
- /// Converts a by performing a ceiling operation on all the coordinates.
- ///
- public static PointF Ceiling(PointF value) => new(unchecked((int)Math.Ceiling(value.X)), unchecked((int)Math.Ceiling(value.Y)));
-
- ///
- /// Converts a by performing a truncate operation on all the coordinates.
- ///
- public static PointF Truncate(PointF value) => new(unchecked((int)value.X), unchecked((int)value.Y));
-
- ///
- /// Converts a by performing a round operation on all the coordinates.
- ///
- public static PointF Round(PointF value) => new(unchecked((int)Math.Round(value.X)), unchecked((int)Math.Round(value.Y)));
-
///
/// Translates this by the specified amount.
///
diff --git a/src/Common/Rectangle.cs b/src/Common/Rectangle.cs
index 6ae6bd9..9bbc717 100644
--- a/src/Common/Rectangle.cs
+++ b/src/Common/Rectangle.cs
@@ -46,8 +46,16 @@ public Rectangle(Point location, int width, int height)
#region Operators
+ ///
+ /// Converts the specified to a .
+ ///
public static explicit operator SKRect(Rectangle rect) => rect.m_rect;
+ ///
+ /// Creates a with the coordinates of the specified .
+ ///
+ public static implicit operator RectangleF(Rectangle rect) => new(rect.X, rect.Y, rect.Width, rect.Height);
+
///
/// Tests whether two objects have equal location and size.
///
@@ -190,6 +198,17 @@ public Point Location
///
public readonly bool IsEmpty => m_rect.IsEmpty;
+ ///
+ /// Gets a secuencie of that defines this .
+ ///
+ public readonly Point[] Points => new[]
+ {
+ new Point(Left, Top),
+ new Point(Right, Top),
+ new Point(Right, Bottom),
+ new Point(Left, Bottom)
+ };
+
#endregion
diff --git a/src/Common/RectangleF.cs b/src/Common/RectangleF.cs
index 56c5ff2..f08db2a 100644
--- a/src/Common/RectangleF.cs
+++ b/src/Common/RectangleF.cs
@@ -46,6 +46,9 @@ public RectangleF(PointF location, float width, float height)
#region Operators
+ ///
+ /// Converts the specified to a .
+ ///
public static explicit operator SKRect(RectangleF rect) => rect.m_rect;
///
@@ -190,6 +193,17 @@ public PointF Location
///
public readonly bool IsEmpty => m_rect.IsEmpty;
+ ///
+ /// Gets a secuencie of that defines this .
+ ///
+ public readonly PointF[] Points => new[]
+ {
+ new PointF(Left, Top),
+ new PointF(Right, Top),
+ new PointF(Right, Bottom),
+ new PointF(Left, Bottom)
+ };
+
#endregion
@@ -286,5 +300,10 @@ public static RectangleF Inflate(RectangleF rect, float x, float y)
///
public void Offset(PointF pos) => Offset(pos.X, pos.Y);
+ ///
+ /// Converts a by performing a round operation on all the coordinates.
+ ///
+ public static Rectangle Truncate(RectangleF rect) => new(unchecked((int)rect.X), unchecked((int)rect.Y), unchecked((int)rect.Width), unchecked((int)rect.Height));
+
#endregion
}
diff --git a/src/Common/Region.cs b/src/Common/Region.cs
new file mode 100644
index 0000000..8bb6e08
--- /dev/null
+++ b/src/Common/Region.cs
@@ -0,0 +1,503 @@
+using System;
+using SkiaSharp;
+using GeneXus.Drawing.Drawing2D;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+
+namespace GeneXus.Drawing;
+
+public sealed class Region : IDisposable
+{
+ internal readonly SKRegion m_region;
+
+ internal Region(SKRegion region)
+ {
+ m_region = region;
+ }
+
+ private Region(SKRect rect)
+ : this(new SKRegion(SKRectI.Round(rect))) { }
+
+ ///
+ /// Initializes a new .
+ ///
+ public Region()
+ : this(new SKRegion()) { }
+
+ ///
+ /// Initializes a new from the specified structure.
+ ///
+ public Region(RectangleF rect)
+ : this(rect.m_rect) { }
+
+ ///
+ /// Initializes a new from the specified structure.
+ ///
+ public Region(Rectangle rect)
+ : this(rect.m_rect) { }
+
+ ///
+ /// Initializes a new from the specified structure.
+ ///
+ public Region(GraphicsPath path)
+ : this(new SKRegion(path.m_path)) { }
+
+ ///
+ /// Initializes a new from the specified data.
+ ///
+ public Region(RegionData data)
+ : this(ParseRegionData(data)) { }
+
+ ///
+ /// Cleans up resources for this .
+ ///
+ ~Region() => Dispose(false);
+
+
+ #region IDisposable
+
+ ///
+ /// Releases all resources used by this .
+ ///
+ public void Dispose()
+ {
+ GC.SuppressFinalize(this);
+ Dispose(true);
+ }
+
+ private void Dispose(bool disposing) => m_region.Dispose();
+
+ #endregion
+
+
+ #region IClonable
+
+ ///
+ /// Creates an exact copy of this .
+ ///
+ public Region Clone() => new(new SKRegion(m_region.GetBoundaryPath()));
+
+ #endregion
+
+
+ #region IEqualitable
+
+ ///
+ /// Determines whether the specified object is equal to the current object.
+ ///
+ public override bool Equals(object obj) => obj is Region region && m_region.Equals(region.m_region);
+
+ ///
+ /// Get the has code of this .
+ ///
+ public override int GetHashCode() => m_region.GetHashCode();
+
+ #endregion
+
+
+ #region Factory
+
+ ///
+ /// Initializes a new from a handle to the specified existing GDI region.
+ ///
+ public static Region FromHrgn(IntPtr hrgn)
+ => throw new NotSupportedException("windows specific");
+
+ #endregion
+
+
+ #region Methods
+
+ ///
+ /// Updates this to contain the portion of the specified that
+ /// does not intersect with this .
+ ///
+ public void Complement(Region region)
+ => m_region.Op(region.m_region, SKRegionOperation.ReverseDifference);
+
+ ///
+ /// Updates this to contain the portion of the specified that
+ /// does not intersect with this .
+ ///
+ public void Complement(RectangleF rect)
+ => Complement(RectangleF.Truncate(rect));
+
+ ///
+ /// Updates this to contain the portion of the specified that
+ /// does not intersect with this .
+ ///
+ public void Complement(Rectangle rect)
+ => Complement(new Region(rect));
+
+ ///
+ /// Updates this to contain the portion of the specified that
+ /// does not intersect with this .
+ ///
+ public void Complement(GraphicsPath path)
+ => Complement(new Region(path));
+
+ ///
+ /// Updates this to contain only the portion of its interior that does not intersect
+ /// with the specified .
+ ///
+ public void Exclude(Region region)
+ => m_region.Op(region.m_region, SKRegionOperation.Difference);
+
+ ///
+ /// Updates this to contain only the portion of its interior that does not intersect
+ /// with the specified .
+ ///
+ public void Exclude(RectangleF rect)
+ => Exclude(RectangleF.Truncate(rect));
+
+ ///
+ /// Updates this to contain only the portion of its interior that does not intersect
+ /// with the specified .
+ ///
+ public void Exclude(Rectangle rect)
+ => Exclude(new Region(rect));
+
+ ///
+ /// Updates this to contain only the portion of its interior that does not intersect
+ /// with the specified .
+ ///
+ public void Exclude(GraphicsPath path)
+ => Exclude(new Region(path));
+
+ ///
+ /// Gets a structure that represents a rectangle that bounds this
+ /// on the drawing surface of a object.
+ ///
+ public RectangleF GetBounds(Graphics g)
+ {
+ var bounds = SKRect.Create(m_region.Bounds.Location, m_region.Bounds.Size);
+ if (g != null)
+ bounds.IntersectsWith(g.m_canvas.LocalClipBounds);
+ return new(bounds);
+ }
+
+ ///
+ /// Returns a Windows handle to this in the specified graphics context.
+ ///
+ public IntPtr GetHrgn(Graphics g)
+ => throw new NotSupportedException("windows specific");
+
+ ///
+ /// Returns a that represents the information that describes this .
+ ///
+ public RegionData GetRegionData()
+ {
+ using var iterator = m_region.CreateRectIterator();
+
+ var rects = new List();
+ while (iterator.Next(out var rect))
+ rects.Add(rect);
+
+ var rdh = new RGNDATAHEADER
+ {
+ dwSize = (uint)Marshal.SizeOf(),
+ iType = 1, // RDH_RECTANGLES
+ nCount = (uint)rects.Count,
+ nRgnSize = (uint)(rects.Count * Marshal.SizeOf())
+ };
+ var headerSize = Marshal.SizeOf();
+ var bufferSize = rects.Count * Marshal.SizeOf();
+ var data = new byte[headerSize + bufferSize];
+
+ // Copy header to data
+ Buffer.BlockCopy(BitConverter.GetBytes(rdh.dwSize), 0, data, 0, 4);
+ Buffer.BlockCopy(BitConverter.GetBytes(rdh.iType), 0, data, 4, 4);
+ Buffer.BlockCopy(BitConverter.GetBytes(rdh.nCount), 0, data, 8, 4);
+ Buffer.BlockCopy(BitConverter.GetBytes(rdh.nRgnSize), 0, data, 12, 4);
+
+ // Copy rectangles to data
+ for (int i = 0; i < rects.Count; i++)
+ {
+ var rectData = new byte[Marshal.SizeOf()];
+ Buffer.BlockCopy(BitConverter.GetBytes(rects[i].Left), 0, rectData, 0, 4);
+ Buffer.BlockCopy(BitConverter.GetBytes(rects[i].Top), 0, rectData, 4, 4);
+ Buffer.BlockCopy(BitConverter.GetBytes(rects[i].Right), 0, rectData, 8, 4);
+ Buffer.BlockCopy(BitConverter.GetBytes(rects[i].Bottom), 0, rectData, 12, 4);
+ Buffer.BlockCopy(rectData, 0, data, headerSize + i * rectData.Length, rectData.Length);
+ }
+
+ return new RegionData(data);
+ }
+
+ ///
+ /// Returns an array of structures that approximate this after
+ /// the specified matrix transformation is applied.
+ ///
+ public RectangleF[] GetRegionScans(Matrix matrix) // TODO: apply Matrix
+ {
+ Transform(matrix);
+ using var iterator = m_region.CreateRectIterator();
+ var rects = new List();
+ while (iterator.Next(out var rect))
+ rects.Add(new RectangleF(rect));
+ return rects.ToArray();
+ }
+
+ ///
+ /// Updates this to the intersection of itself with the specified .
+ ///
+ public void Intersect(Region region)
+ => m_region.Op(region.m_region, SKRegionOperation.Intersect);
+
+ ///
+ /// Updates this to the intersection of itself with the specified .
+ ///
+ public void Intersect(RectangleF rect)
+ => Intersect(RectangleF.Truncate(rect));
+
+ ///
+ /// Updates this to the intersection of itself with the specified .
+ ///
+ public void Intersect(Rectangle rect)
+ => Intersect(new Region(rect));
+
+ ///
+ /// Updates this to the intersection of itself with the specified .
+ ///
+ public void Intersect(GraphicsPath path)
+ => Intersect(new Region(path));
+
+ ///
+ /// Tests whether this has an empty interior on the specified drawing surface
+ /// of a object.
+ ///
+ public bool IsEmpty(Graphics g)
+ {
+ var region = new SKRegion(m_region);
+ if (g != null)
+ region.Intersects(g.Clip.m_region);
+ return region.IsEmpty;
+ }
+
+ ///
+ /// Tests whether this has an infinite interior on the specified drawing surface
+ /// of a object.
+ ///
+ public bool IsInfinite(Graphics g)
+ {
+ var region = new SKRegion(m_region);
+ if (g != null)
+ region.Intersects(g.Clip.m_region);
+ return !region.IsRect;
+ }
+
+ ///
+ /// Tests whether any portion of the specified rectangle is contained within this when drawn
+ /// using the specified (if it is defined).
+ ///
+ public bool IsVisible(int x, int y, int width, int height, Graphics g = null)
+ => IsVisible(new Rectangle(x, y, width, height), g);
+
+ ///
+ /// Tests whether any portion of the specified rectangle is contained within this when drawn
+ /// using the specified (if it is defined).
+ ///
+ public bool IsVisible(float x, float y, float width, float height, Graphics g = null)
+ => IsVisible(new RectangleF(x, y, width, height), g);
+
+ ///
+ /// Tests whether any portion of the specified structure is contained within
+ /// this when drawn using the specified (if it is defined).
+ ///
+ public bool IsVisible(RectangleF rect, Graphics g = null)
+ => IsVisible(RectangleF.Truncate(rect), g);
+
+ ///
+ /// Tests whether any portion of the specified structure is contained within
+ /// this when drawn using the specified (if it is defined).
+ ///
+ public bool IsVisible(Rectangle rect, Graphics g = null)
+ {
+ var region = new SKRegion(m_region);
+ if (g != null)
+ region.Intersects(g.Clip.m_region);
+ return region.Contains(SKRectI.Round(rect.m_rect));
+ }
+
+ ///
+ /// Tests whether the specified point is contained within this when drawn
+ /// using the specified (if it is defined).
+ ///
+ public bool IsVisible(int x, int y, Graphics g = null)
+ => IsVisible(new Point(x, y), g);
+
+ ///
+ /// Tests whether the specified point is contained within this when drawn
+ /// using the specified (if it is defined).
+ ///
+ public bool IsVisible(float x, float y, Graphics g = null)
+ => IsVisible(new PointF(x, y), g);
+
+ ///
+ /// Tests whether the specified structure is contained within
+ /// this when drawn using the specified (if it is defined).
+ ///
+ public bool IsVisible(PointF point, Graphics g = null)
+ => IsVisible(Point.Truncate(point), g);
+
+ ///
+ /// Tests whether the specified structure is contained within
+ /// this when drawn using the specified (if it is defined).
+ ///
+ public bool IsVisible(Point point, Graphics g = null)
+ {
+ var region = new SKRegion(m_region);
+ if (g != null)
+ {
+ var points = new[] { point };
+ g.Transform.TransformPoints(points);
+ point = points[0];
+
+ region.Intersects(g.Clip.m_region);
+ }
+ return region.Contains(point.X, point.Y);
+ }
+
+ ///
+ /// Initializes this to an empty interior.
+ ///
+ public void MakeEmpty()
+ => m_region.SetEmpty();
+
+ ///
+ /// Initializes this to an empty interior.
+ ///
+ public void MakeInfinite()
+ => m_region.SetRect(SKRectI.Create(-4194304, -4194304, 8388608, 8388608));
+
+ ///
+ /// Releases the handle of the .
+ ///
+ public void ReleaseHrgn(IntPtr regionHandle)
+ => throw new NotImplementedException();
+
+ ///
+ /// Transforms this by the specified .
+ ///
+ public void Transform(Matrix matrix)
+ {
+ var path = m_region.GetBoundaryPath();
+ path.Transform(matrix.m_matrix);
+ m_region.SetPath(path);
+ }
+
+ ///
+ /// Offsets the coordinates of this by the specified amount.
+ ///
+ public void Translate(float dx, float dy)
+ => m_region.Translate((int)Math.Round(dx), (int)Math.Round(dy));
+
+ ///
+ /// Updates this to the union of itself with the specified .
+ ///
+ public void Union(Region region)
+ => m_region.Op(region.m_region, SKRegionOperation.Union);
+
+ ///
+ /// Updates this to the union of itself with the specified .
+ ///
+ public void Union(RectangleF rect)
+ => Union(RectangleF.Truncate(rect));
+
+ ///
+ /// Updates this to the union of itself with the specified .
+ ///
+ public void Union(Rectangle rect)
+ => Union(new Region(rect));
+
+ ///
+ /// Updates this to the union of itself with the specified .
+ ///
+ public void Union(GraphicsPath path)
+ => Union(new Region(path));
+
+ ///
+ /// Updates this to the union minus the intersection of itself with the specified .
+ ///
+ public void Xor(Region region)
+ => m_region.Op(region.m_region, SKRegionOperation.XOR);
+
+ ///
+ /// Updates this to the union minus the intersection of itself with the specified .
+ ///
+ public void Xor(RectangleF rect)
+ => Xor(RectangleF.Truncate(rect));
+
+ ///
+ /// Updates this to the union minus the intersection of itself with the specified .
+ ///
+ public void Xor(Rectangle rect)
+ => Xor(new Region(rect));
+
+ ///
+ /// Updates this to the union minus the intersection of itself with the specified .
+ ///
+ public void Xor(GraphicsPath path)
+ => Xor(new Region(path));
+
+ #endregion
+
+
+ #region Utilities
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct RECT
+ {
+ public int Left;
+ public int Top;
+ public int Right;
+ public int Bottom;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct RGNDATAHEADER
+ {
+ public uint dwSize;
+ public uint iType;
+ public uint nCount;
+ public uint nRgnSize;
+ }
+
+ private static SKRegion ParseRegionData(RegionData data)
+ {
+ var rgnData = data.Data;
+ var headerSize = Marshal.SizeOf();
+ var rectSize = Marshal.SizeOf();
+
+ if (rgnData.Length < headerSize)
+ throw new ArgumentException("invalid region data.");
+
+ var rdh = new RGNDATAHEADER
+ {
+ dwSize = BitConverter.ToUInt32(rgnData, 0),
+ iType = BitConverter.ToUInt32(rgnData, 4),
+ nCount = BitConverter.ToUInt32(rgnData, 8),
+ nRgnSize = BitConverter.ToUInt32(rgnData, 12)
+ };
+
+ if (rgnData.Length < headerSize + rdh.nRgnSize)
+ throw new ArgumentException("invalid region data.");
+
+ var region = new SKRegion();
+ for (int i = 0; i < rdh.nCount; i++)
+ {
+ var offset = headerSize + i * rectSize;
+ var rect = new SKRectI
+ {
+ Left = BitConverter.ToInt32(rgnData, offset + 0),
+ Top = BitConverter.ToInt32(rgnData, offset + 4),
+ Right = BitConverter.ToInt32(rgnData, offset + 8),
+ Bottom = BitConverter.ToInt32(rgnData, offset + 12)
+ };
+ region.Op(rect, SKRegionOperation.Union);
+ }
+ return region;
+ }
+
+ #endregion
+}
diff --git a/src/Common/SolidBrush.cs b/src/Common/SolidBrush.cs
new file mode 100644
index 0000000..cf791a5
--- /dev/null
+++ b/src/Common/SolidBrush.cs
@@ -0,0 +1,35 @@
+
+using SkiaSharp;
+
+namespace GeneXus.Drawing;
+
+public sealed class SolidBrush : Brush
+{
+ public SolidBrush(Color color)
+ : base(new SKPaint { Color = color.m_color }) { }
+
+
+ #region IClonable
+
+ ///
+ /// Creates an exact copy of this .
+ ///
+ public override object Clone()
+ => new SolidBrush(new Color(m_paint.Color));
+
+ #endregion
+
+
+ #region Properties
+
+ ///
+ /// Gets or sets the color of this object.
+ ///
+ public Color Color
+ {
+ get => new(m_paint.Color);
+ set => m_paint.Color = value.m_color;
+ }
+
+ #endregion
+}
diff --git a/src/Common/StringAlignment.cs b/src/Common/StringAlignment.cs
new file mode 100644
index 0000000..b062884
--- /dev/null
+++ b/src/Common/StringAlignment.cs
@@ -0,0 +1,26 @@
+namespace GeneXus.Drawing;
+
+///
+/// Specifies the alignment of a text string relative to its layout rectangle.
+///
+public enum StringAlignment
+{
+ // left or top in English
+ ///
+ /// Specifies the text be aligned near the layout. In a left-to-right layout, the near position is left. In a
+ /// right-to-left layout, the near position is right.
+ ///
+ Near = 0,
+
+ ///
+ /// Specifies that text is aligned in the center of the layout rectangle.
+ ///
+ Center = 1,
+
+ // right or bottom in English
+ ///
+ /// Specifies that text is aligned far from the origin position of the layout rectangle. In a left-to-right
+ /// layout, the far position is right. In a right-to-left layout, the far position is left.
+ ///
+ Far = 2
+}
diff --git a/src/Common/StringDigitSubstitute.cs b/src/Common/StringDigitSubstitute.cs
new file mode 100644
index 0000000..972d867
--- /dev/null
+++ b/src/Common/StringDigitSubstitute.cs
@@ -0,0 +1,28 @@
+namespace GeneXus.Drawing;
+
+///
+/// Specifies style information applied to String Digit Substitute.
+///
+public enum StringDigitSubstitute
+{
+ ///
+ /// Specifies a user-defined substitution scheme.
+ ///
+ User = 0,
+
+ ///
+ /// Specifies to disable substitutions.
+ ///
+ None = 1,
+
+ ///
+ /// Specifies substitution digits that correspond with the official national language of the user's locale.
+ ///
+ National = 2,
+
+ ///
+ /// Specifies substitution digits that correspond with the user's native script or language, which may be
+ /// different from the official national language of the user's locale.
+ ///
+ Traditional = 3
+}
diff --git a/src/Common/StringFormat.cs b/src/Common/StringFormat.cs
new file mode 100644
index 0000000..1b97791
--- /dev/null
+++ b/src/Common/StringFormat.cs
@@ -0,0 +1,491 @@
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+using GeneXus.Drawing.Text;
+using SkiaSharp;
+
+namespace GeneXus.Drawing;
+
+public sealed class StringFormat : ICloneable, IDisposable
+{
+ private float TabOffset = 0;
+ private float[] TabStops = Array.Empty();
+ private CharacterRange[] Ranges = Array.Empty();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public StringFormat()
+ : this(0, 0) { }
+
+ ///
+ /// Initializes a new instance of the class with the specified .
+ ///
+ public StringFormat(StringFormatFlags options)
+ : this(options, 0) { }
+
+ ///
+ /// Initializes a new instance of the class with the specified
+ /// and language.
+ ///
+ public StringFormat(StringFormatFlags options, int language)
+ {
+ FormatFlags = options;
+ DigitSubstitutionLanguage = language;
+ }
+
+ ///
+ /// Initializes a new instance of the class from the specified
+ /// existing .
+ ///
+ public StringFormat(StringFormat format)
+ => format.MemberwiseClone();
+
+ ///
+ /// Cleans up resources for this .
+ ///
+ ~StringFormat() => Dispose(false);
+
+ ///
+ /// Converts this to a human-readable string.
+ ///
+ public override string ToString() => $"[StringFormat, FormatFlags={FormatFlags}]";
+
+
+ #region IDisposable
+
+ ///
+ /// Cleans up resources for this .
+ ///
+ public void Dispose()
+ {
+ GC.SuppressFinalize(this);
+ Dispose(true);
+ }
+
+ private void Dispose(bool disposing) { }
+
+ #endregion
+
+
+ #region IClonable
+
+ ///
+ /// Creates an exact copy of this .
+ ///
+ public object Clone() => new StringFormat(this);
+
+ #endregion
+
+
+ #region Properties
+
+ ///
+ /// Specifies text alignment information.
+ ///
+ public StringAlignment Alignment { get; set; } = StringAlignment.Near;
+
+ ///
+ /// Gets the for this .
+ ///
+ public StringDigitSubstitute DigitSubstitutionMethod { get; private set; } = StringDigitSubstitute.None;
+
+ ///
+ /// Gets the language of for this .
+ ///
+ public int DigitSubstitutionLanguage { get; private set; } = CultureInfo.InvariantCulture.LCID;
+
+ ///
+ /// Gets or sets a that contains formatting information.
+ ///
+ public StringFormatFlags FormatFlags { get; set; } = 0;
+
+ ///
+ /// Gets a generic default .
+ ///
+ public static StringFormat GenericDefault => new();
+
+ ///
+ /// Gets a generic typographic .
+ ///
+ public static StringFormat GenericTypographic => new()
+ {
+ FormatFlags = StringFormatFlags.FitBlackBox | StringFormatFlags.LineLimit | StringFormatFlags.NoClip,
+ Trimming = StringTrimming.None
+ };
+
+ ///
+ /// Gets or sets the for this .
+ ///
+ public HotkeyPrefix HotkeyPrefix { get; set; } = HotkeyPrefix.None;
+
+ ///
+ /// Gets or sets the line alignment.
+ ///
+ public StringAlignment LineAlignment { get; set; } = StringAlignment.Near;
+
+ ///
+ /// Gets or sets the for this .
+ ///
+ public StringTrimming Trimming { get; set; } = StringTrimming.None;
+
+ #endregion
+
+
+ #region Methods
+
+ ///
+ /// Gets the ranges count for this object.
+ ///
+ internal int GetMeasurableCharacterRangeCount()
+ => Ranges.Length;
+
+ ///
+ /// Gets the tab stops for this object.
+ ///
+ public float[] GetTabStops(out float firstTabOffset)
+ {
+ firstTabOffset = TabOffset;
+ return TabStops;
+ }
+
+ ///
+ /// Specifies the language and method to be used when local digits are substituted for western digits.
+ ///
+ public void SetDigitSubstitution(int language, StringDigitSubstitute substitute)
+ {
+ DigitSubstitutionLanguage = language;
+ DigitSubstitutionMethod = substitute;
+ }
+
+ ///
+ /// Sets the measure of characters to the specified range.
+ ///
+ public void SetMeasurableCharacterRanges(CharacterRange[] ranges)
+ => Ranges = ranges;
+
+ ///
+ /// Sets tab stops for this .
+ ///
+ public void SetTabStops(float firstTabOffset, float[] tabStops)
+ {
+ TabOffset = firstTabOffset;
+ TabStops = tabStops;
+ }
+
+ #endregion
+
+
+ #region Utilties
+
+ internal static readonly string[] BREAKLINES = new string[]
+ {
+ "\r\n",
+ "\n",
+ "\u2028", // unicode's line separator
+ "\u2029", // unicode's paragraph separator
+ "\u0085", // unicode's next line
+ };
+
+ internal static readonly char[] CONTROL = Enumerable.Range(0x00, 0xFFFF)
+ .Select(Convert.ToChar)
+ .Where(char.IsControl)
+ .ToArray();
+
+ internal static readonly Dictionary CONTROL_CHARACTERS = new()
+ {
+ { '\u0000', '␀' }, // Null
+ { '\u0001', '␁' }, // Start of Heading
+ { '\u0002', '␂' }, // Start of Text
+ { '\u0003', '␃' }, // End of Text
+ { '\u0004', '␄' }, // End of Transmission
+ { '\u0005', '␅' }, // Enquiry
+ { '\u0006', '␆' }, // Acknowledge
+ { '\u0007', '␇' }, // Bell
+ { '\u0008', '␈' }, // Backspace
+ { '\u0009', '␉' }, // Horizontal Tab
+ { '\u000A', '␊' }, // Line Feed
+ { '\u000B', '␋' }, // Vertical Tab
+ { '\u000C', '␌' }, // Form Feed
+ { '\u000D', '␍' }, // Carriage Return
+ { '\u000E', '␎' }, // Shift Out
+ { '\u000F', '␏' }, // Shift In
+ { '\u0010', '␐' }, // Data Link Escape
+ { '\u0011', '␑' }, // Device Control 1
+ { '\u0012', '␒' }, // Device Control 2
+ { '\u0013', '␓' }, // Device Control 3
+ { '\u0014', '␔' }, // Device Control 4
+ { '\u0015', '␕' }, // Negative Acknowledge
+ { '\u0016', '␖' }, // Synchronous Idle
+ { '\u0017', '␗' }, // End of Transmission Block
+ { '\u0018', '␘' }, // Cancel
+ { '\u0019', '␙' }, // End of Medium
+ { '\u001A', '␚' }, // Substitute
+ { '\u001B', '␛' }, // Escape
+ { '\u001C', '␜' }, // File Separator
+ { '\u001D', '␝' }, // Group Separator
+ { '\u001E', '␞' }, // Record Separator
+ { '\u001F', '␟' }, // Unit Separator
+ { '\u007F', '␡' }, // Delete
+ { '\u0085', '' }, // Next Line
+ { '\u00A0', '␣' }, // Non-Breaking Space
+ { '\u1680', ' ' }, // Ogham Space Mark
+ { '\u2000', ' ' }, // En Quad
+ { '\u2001', ' ' }, // Em Quad
+ { '\u2002', ' ' }, // En Space
+ { '\u2003', ' ' }, // Em Space
+ { '\u2004', ' ' }, // Three-Per-Em Space
+ { '\u2005', ' ' }, // Four-Per-Em Space
+ { '\u2006', ' ' }, // Six-Per-Em Space
+ { '\u2007', ' ' }, // Figure Space
+ { '\u2008', ' ' }, // Punctuation Space
+ { '\u2009', ' ' }, // Thin Space
+ { '\u200A', ' ' }, // Hair Space
+ { '\u200B', '' }, // Zero Width Space
+ { '\u200C', '' }, // Zero Width Non-Joiner
+ { '\u200D', '' }, // Zero Width Joiner
+ { '\u200E', '' }, // Left-to-Right Mark
+ { '\u200F', '' }, // Right-to-Left Mark
+ { '\u2028', '' }, // Line Separator
+ { '\u2029', '¶' }, // Paragraph Separator
+ { '\u202A', '' }, // Left-to-Right Embedding
+ { '\u202B', '' }, // Right-to-Left Embedding
+ { '\u202C', '' }, // Pop Directional Formatting
+ { '\u202D', '' }, // Left-to-Right Override
+ { '\u202E', '' }, // Right-to-Left Override
+ { '\u2060', '' }, // Word Joiner
+ { '\u2061', '' }, // Function Application
+ { '\u2062', '' }, // Invisible Times
+ { '\u2063', '' }, // Invisible Separator
+ { '\u2064', '' } // Invisible Plus
+ };
+
+ #endregion
+
+
+ #region Helpers
+
+ internal string[] ApplyRanges(string text)
+ {
+ var substrings = new List();
+ foreach (var range in Ranges)
+ {
+ int idx = Math.Max(0, range.First);
+ int end = Math.Min(idx + range.Length, idx + text.Length);
+ if (idx == end)
+ continue;
+ string substring = text.Substring(idx, end - idx);
+ substrings.Add(substring);
+ }
+ return substrings.ToArray();
+ }
+
+ internal string ApplyDirection(string text)
+ => FormatFlags.HasFlag(StringFormatFlags.DirectionVertical)
+ ? string.Join("\n", text.Split(BREAKLINES, StringSplitOptions.None).Reverse())
+ : text;
+
+
+ internal string ApplyHotkey(string text, out int[] indexes)
+ {
+ var hkIndexes = new List();
+
+ var sb = new StringBuilder();
+ for (int i = 0; i < text.Length; i++)
+ {
+ var c = text[i];
+ if (c == '&')
+ {
+ if (i < text.Length - 1 && text[i + 1] == '&')
+ {
+ i++; // skip next '&'
+ }
+ else
+ {
+ if (HotkeyPrefix == HotkeyPrefix.Show)
+ hkIndexes.Add(sb.Length);
+ if (HotkeyPrefix != HotkeyPrefix.None)
+ continue;
+ }
+ }
+ sb.Append(c);
+ }
+ indexes = hkIndexes.ToArray();
+ return sb.ToString();
+ }
+
+
+ internal string ApplyTabStops(string text)
+ {
+ var tabStops = GetTabStops(out var tabOffset);
+ if (tabStops.Length > 0)
+ {
+ var fill = string.Empty.PadLeft((int)tabOffset, ' ');
+
+ var lines = text.Split('\n');
+ for (int i = 0; i < lines.Length; i++)
+ {
+ var columns = lines[i].Split('\t');
+ for (int j = 0; j < columns.Length; j++)
+ {
+ int pad = j < tabStops.Length ? (int)tabStops[j] : columns[j].Length;
+ columns[j] = columns[j].PadLeft(pad, ' ');
+ }
+ lines[i] = fill + string.Join("", columns);
+ }
+ text = string.Join("", lines);
+ }
+ return text;
+ }
+
+
+ internal string ApplyDigitSubstitution(string text)
+ {
+ var sb = new StringBuilder(text);
+ if (DigitSubstitutionMethod != StringDigitSubstitute.None)
+ {
+ var lcid = DigitSubstitutionMethod == StringDigitSubstitute.User
+ ? CultureInfo.CurrentCulture.LCID
+ : DigitSubstitutionLanguage;
+
+ var culture = new CultureInfo(lcid);
+
+ var format = culture.NumberFormat;
+ for (int i = 0; i < format.NativeDigits.Length; i++)
+ sb.Replace(i.ToString(), format.NativeDigits[i]);
+ }
+ return sb.ToString();
+ }
+
+
+ internal string ApplyControlEscape(string text)
+ {
+ var sb = new StringBuilder(text);
+ if (FormatFlags.HasFlag(StringFormatFlags.DisplayFormatControl))
+ {
+ var controlChars = CONTROL_CHARACTERS;
+ foreach (char chr in CONTROL)
+ if (!controlChars.ContainsKey(chr))
+ controlChars.Add(chr, '�');
+
+ foreach (var kvp in controlChars)
+ sb.Replace(kvp.Key, kvp.Value);
+ }
+ return sb.ToString();
+ }
+
+
+ internal string ApplyWrapping(string text, SKRect boundBox, Func measureText)
+ {
+ if (FormatFlags.HasFlag(StringFormatFlags.NoWrap))
+ return text;
+
+ float accWidth = 0;
+
+ var sb = new StringBuilder();
+ foreach (var line in text.Split(BREAKLINES, StringSplitOptions.None))
+ {
+ foreach (var word in line.Split(' '))
+ {
+ var curWidth = measureText(word);
+ if (Math.Round(accWidth += curWidth) > boundBox.Width)
+ {
+ sb.Append('\n').Append(' ');
+ accWidth = curWidth;
+ }
+ sb.Append(word).Append(' ');
+ }
+ sb.Length--; // remove last space
+ }
+ return sb.ToString();
+ }
+
+
+ private string ApplyTrimming(IEnumerable tokens, string endToken, SKRect boundBox, Func measureText)
+ {
+ float accWidth = 0;
+ float endLen = measureText(endToken);
+
+ var sb = new StringBuilder();
+ var tokenList = tokens.ToArray();
+ for (int i = 0; i < tokenList.Length; i++)
+ {
+ string token = tokenList[i];
+
+ float curWidth = measureText(token);
+ if (token == "&")
+ {
+ if (i < tokenList.Length - 1 && tokenList[i + 1] == "&")
+ {
+ i++; // skip next '&'
+ }
+ else if (HotkeyPrefix != HotkeyPrefix.None)
+ {
+ curWidth = 0;
+ }
+ }
+
+ if (Math.Round((accWidth += curWidth) + endLen) > boundBox.Width)
+ {
+ if (FormatFlags.HasFlag(StringFormatFlags.DirectionRightToLeft))
+ sb.Insert(0, endToken);
+ else
+ sb.Append(endToken);
+ break;
+ }
+ sb.Append(token);
+ }
+
+ var trim = (string s) => FormatFlags.HasFlag(StringFormatFlags.DirectionRightToLeft) ? s.TrimStart() : s.TrimEnd();
+ return trim(sb.ToString());
+ }
+
+
+ internal string ApplyTrimming(string text, SKRect boundBox, float lineHeight, Func measureText)
+ {
+ var lines = text.Split(BREAKLINES, StringSplitOptions.None);
+ if (FormatFlags.HasFlag(StringFormatFlags.LineLimit))
+ lines = lines.Take((int)(boundBox.Height / lineHeight)).ToArray();
+
+ for (int i = 0; i < lines.Length; i++)
+ {
+ string line = lines[i];
+
+ if (!FormatFlags.HasFlag(StringFormatFlags.MeasureTrailingSpaces))
+ line = FormatFlags.HasFlag(StringFormatFlags.DirectionRightToLeft) ? line.TrimStart() : line.TrimEnd();
+
+ line = Trimming switch
+ {
+ StringTrimming.None
+ => line,
+
+ StringTrimming.Character
+ => ApplyTrimming(line.Select(chr => chr.ToString()), "", boundBox, measureText),
+
+ StringTrimming.Word
+ => ApplyTrimming(line.Split(' '), "", boundBox, measureText),
+
+ StringTrimming.EllipsisCharacter
+ => ApplyTrimming(line.Select(chr => chr.ToString()), "…", boundBox, measureText),
+
+ StringTrimming.EllipsisWord
+ => ApplyTrimming(line.Split(' '), "…", boundBox, measureText),
+
+ StringTrimming.EllipsisPath
+ => ApplyTrimming(line.Split(Path.PathSeparator), "…", boundBox, measureText),
+
+ _ => throw new NotImplementedException($"trimming value {Trimming}")
+ };
+
+ lines[i] = line;
+ }
+
+ return string.Join("\n", lines).Trim();
+ }
+
+ #endregion
+}
diff --git a/src/Common/StringFormatFlags.cs b/src/Common/StringFormatFlags.cs
new file mode 100644
index 0000000..ee2c93e
--- /dev/null
+++ b/src/Common/StringFormatFlags.cs
@@ -0,0 +1,75 @@
+using System;
+
+namespace GeneXus.Drawing;
+
+///
+/// Specifies the display and layout information for text strings.
+///
+[Flags]
+public enum StringFormatFlags
+{
+ ///
+ /// Specifies that text is right to left.
+ ///
+ DirectionRightToLeft = 0x00000001,
+
+ ///
+ /// Specifies that text is vertical.
+ ///
+ DirectionVertical = 0x00000002,
+
+ ///
+ /// Specifies that no part of any glyph overhangs the bounding rectangle. By default some glyphs
+ /// overhang the rectangle slightly where necessary to appear at the edge visually. For example
+ /// when an italic lower case letter f in a font such as Garamond is aligned at the far left
+ /// of a rectangle, the lower part of the f will reach slightly further left than the left edge
+ /// of the rectangle. Setting this flag will ensure no painting outside the rectangle but will
+ /// cause the aligned edges of adjacent lines of text to appear uneven.
+ ///
+ FitBlackBox = 0x00000004,
+
+ ///
+ /// Causes control characters such as the left-to-right mark to be shown in the output with a representative glyph.
+ ///
+ DisplayFormatControl = 0x00000020,
+
+ ///
+ /// Disables fallback to alternate fonts for characters not supported in the requested font. Any missing characters are
+ /// displayed with the fonts missing glyph, usually an open square.
+ ///
+ NoFontFallback = 0x00000400,
+
+ ///
+ /// Specifies that the space at the end of each line is included in a string measurement.
+ ///
+ MeasureTrailingSpaces = 0x00000800,
+
+ ///
+ /// Specifies that the wrapping of text to the next line is disabled. NoWrap is implied when a point of origin
+ /// is used instead of a layout rectangle. When drawing text within a rectangle, by default, text is broken at
+ /// the last word boundary that is inside the rectangle's boundary and wrapped to the next line.
+ ///
+ NoWrap = 0x00001000,
+
+ ///
+ /// Specifies that only entire lines are laid out in the layout rectangle. By default, layout
+ /// continues until the end of the text or until no more lines are visible as a result of clipping,
+ /// whichever comes first. The default settings allow the last line to be partially obscured by a
+ /// layout rectangle that is not a whole multiple of the line height.
+ /// To ensure that only whole lines are seen, set this flag and be careful to provide a layout
+ /// rectangle at least as tall as the height of one line.
+ ///
+ LineLimit = 0x00002000,
+
+ ///
+ /// Specifies that characters overhanging the layout rectangle and text extending outside the layout
+ /// rectangle are allowed to show. By default, all overhanging characters and text that extends outside
+ /// the layout rectangle are clipped. Any trailing spaces (spaces that are at the end of a line) that
+ /// extend outside the layout rectangle are clipped. Therefore, the setting of this flag will have an
+ /// effect on a string measurement if trailing spaces are being included in the measurement.
+ /// If clipping is enabled, trailing spaces that extend outside the layout rectangle are not included
+ /// in the measurement. If clipping is disabled, all trailing spaces are included in the measurement,
+ /// regardless of whether they are outside the layout rectangle.
+ ///
+ NoClip = 0x00004000
+}
diff --git a/src/Common/StringTrimming.cs b/src/Common/StringTrimming.cs
new file mode 100644
index 0000000..a7fc3e1
--- /dev/null
+++ b/src/Common/StringTrimming.cs
@@ -0,0 +1,41 @@
+namespace GeneXus.Drawing;
+
+///
+/// Specifies how to trim characters from a string that does not completely fit into a layout shape.
+///
+public enum StringTrimming
+{
+ ///
+ /// Specifies no trimming.
+ ///
+ None = 0,
+
+ ///
+ /// Specifies that the string is broken at the boundary of the last character
+ /// that is inside the layout rectangle. This is the default.
+ ///
+ Character = 1,
+
+ ///
+ /// Specifies that the string is broken at the boundary of the last word that is inside the layout rectangle.
+ ///
+ Word = 2,
+
+ ///
+ /// Specifies that the string is broken at the boundary of the last character that is inside
+ /// the layout rectangle and an ellipsis (...) is inserted after the character.
+ ///
+ EllipsisCharacter = 3,
+
+ ///
+ /// Specifies that the string is broken at the boundary of the last word that is inside the
+ /// layout rectangle and an ellipsis (...) is inserted after the word.
+ ///
+ EllipsisWord = 4,
+
+ ///
+ /// Specifies that the center is removed from the string and replaced by an ellipsis.
+ /// The algorithm keeps as much of the last portion of the string as possible.
+ ///
+ EllipsisPath = 5
+}
diff --git a/src/Common/Text/HotkeyPrefix.cs b/src/Common/Text/HotkeyPrefix.cs
new file mode 100644
index 0000000..dd3a28a
--- /dev/null
+++ b/src/Common/Text/HotkeyPrefix.cs
@@ -0,0 +1,22 @@
+namespace GeneXus.Drawing.Text;
+
+///
+/// Specifies the type of display for hotkey prefixes for text.
+///
+public enum HotkeyPrefix
+{
+ ///
+ /// No hotkey prefix.
+ ///
+ None = 0,
+
+ ///
+ /// Display the hotkey prefix.
+ ///
+ Show = 1,
+
+ ///
+ /// Do not display the hotkey prefix.
+ ///
+ Hide = 2
+}
diff --git a/src/Common/Text/TextRenderingHint.cs b/src/Common/Text/TextRenderingHint.cs
new file mode 100644
index 0000000..b7dcdb1
--- /dev/null
+++ b/src/Common/Text/TextRenderingHint.cs
@@ -0,0 +1,37 @@
+namespace GeneXus.Drawing.Text;
+
+///
+/// Specifies the quality of text rendering.
+///
+public enum TextRenderingHint
+{
+ ///
+ /// Glyph with system default rendering hint.
+ ///
+ SystemDefault = 0,
+
+ ///
+ /// Glyph bitmap with hinting.
+ ///
+ SingleBitPerPixelGridFit = 1,
+
+ ///
+ /// Glyph bitmap without hinting.
+ ///
+ SingleBitPerPixel = 2,
+
+ ///
+ /// Anti-aliasing with hinting.
+ ///
+ AntiAliasGridFit = 3,
+
+ ///
+ /// Glyph anti-alias bitmap without hinting.
+ ///
+ AntiAlias = 4,
+
+ ///
+ /// Glyph CT bitmap with hinting.
+ ///
+ ClearTypeGridFit = 5
+}
diff --git a/src/Common/TextureBrush.cs b/src/Common/TextureBrush.cs
new file mode 100644
index 0000000..0051372
--- /dev/null
+++ b/src/Common/TextureBrush.cs
@@ -0,0 +1,180 @@
+using System;
+using SkiaSharp;
+using GeneXus.Drawing.Drawing2D;
+
+namespace GeneXus.Drawing;
+
+public sealed class TextureBrush : Brush
+{
+ internal Image m_image;
+ internal RectangleF m_bounds;
+ internal WrapMode m_mode;
+
+ private TextureBrush(RectangleF rect, Image image, WrapMode mode)
+ : base(new SKPaint { })
+ {
+ m_bounds = rect;
+ m_image = image;
+ m_mode = mode;
+
+ UpdateShader(() => { });
+ }
+
+ ///
+ /// Initializes a new object that uses the
+ /// specified image and wrap mode.
+ ///
+ public TextureBrush(Image image, WrapMode mode = WrapMode.Tile)
+ : this(image, mode, new Rectangle(0, 0, image.Size)) { }
+
+ ///
+ /// Initializes a new object that uses the
+ /// specified image, wrap mode, and bounding rectangle.
+ ///
+ public TextureBrush(Image image, WrapMode mode, RectangleF rect)
+ : this(rect, image, mode) { }
+
+ ///
+ /// Initializes a new object that uses the
+ /// specified image, wrap mode, and bounding rectangle.
+ ///
+ public TextureBrush(Image image, WrapMode mode, Rectangle rect)
+ : this(image, mode, new RectangleF(rect.m_rect)) { }
+
+ ///
+ /// Initializes a new object that uses
+ /// the specified image, bounding rectangle, and image attributes.
+ ///
+ public TextureBrush(Image image, RectangleF rect, object imageAttributes)
+ : this(image, WrapMode.Tile, rect) { } // TODO: implement ImageAttributes class
+
+ ///
+ /// Initializes a new object that uses
+ /// the specified image, bounding rectangle, and image attributes.
+ ///
+ public TextureBrush(Image image, Rectangle rect, object imageAttributes)
+ : this(image, new RectangleF(rect.m_rect), imageAttributes) { }
+
+
+ #region IClonable
+
+ ///
+ /// Creates an exact copy of this .
+ ///
+ public override object Clone()
+ => new TextureBrush(m_bounds, m_image, m_mode)
+ {
+ Transform = Transform.Clone() as Matrix
+ };
+
+ #endregion
+
+
+ #region Properties
+
+ ///
+ /// Gets the object associated with
+ /// this object.
+ ///
+ public Image Image => m_image;
+
+ ///
+ /// Gets or sets a copy of the object
+ /// that defines a local geometric transformation for the image associated
+ /// with this object.
+ ///
+ public Matrix Transform { get; set; } = new Matrix();
+
+ ///
+ /// Gets or sets a enumeration that
+ /// indicates the wrap mode for this object.
+ ///
+ public WrapMode WrapMode
+ {
+ get => m_mode;
+ set => UpdateShader(() => m_mode = value);
+ }
+
+ #endregion
+
+
+ #region Methods
+
+ ///
+ /// Multiplies the object that represents the local
+ /// geometric transformation of this object
+ /// by the specified object in the specified order.
+ ///
+ public void MultiplyTransform(Matrix matrix, MatrixOrder order = MatrixOrder.Prepend)
+ => Transform.Multiply(matrix, order);
+
+ ///
+ /// Resets the Transform property of this object to identity.
+ ///
+ public void ResetTransform()
+ => Transform.Reset();
+
+ ///
+ /// Rotates the local geometric transformation of this object
+ /// by the specified amount in the specified order.
+ ///
+ public void RotateTransform(float angle, MatrixOrder order = MatrixOrder.Prepend)
+ => Transform.Rotate(angle, order);
+
+ ///
+ /// Scales the local geometric transformation of this object
+ /// by the specified amounts in the specified order.
+ ///
+ public void ScaleTransform(float sx, float sy, MatrixOrder order = MatrixOrder.Prepend)
+ => Transform.Scale(sx, sy, order);
+
+ ///
+ /// Translates the local geometric transformation of this object
+ /// by the specified dimensions in the specified order.
+ ///
+ public void TranslateTransform(float dx, float dy, MatrixOrder order)
+ => Transform.Translate(dx, dy, order);
+
+ #endregion
+
+
+ #region Utilities
+
+ private void UpdateShader(Action action)
+ {
+ action();
+
+ (var tmx, var tmy) = WrapMode switch
+ {
+ WrapMode.Tile => (SKShaderTileMode.Repeat, SKShaderTileMode.Repeat),
+ WrapMode.Clamp => (SKShaderTileMode.Decal, SKShaderTileMode.Decal),
+ WrapMode.TileFlipX => (SKShaderTileMode.Mirror, SKShaderTileMode.Repeat),
+ WrapMode.TileFlipY => (SKShaderTileMode.Repeat, SKShaderTileMode.Mirror),
+ WrapMode.TileFlipXY => (SKShaderTileMode.Mirror, SKShaderTileMode.Mirror),
+ _ => throw new NotImplementedException()
+ };
+
+ var info = new SKImageInfo((int)m_bounds.Width, (int)m_bounds.Height);
+ var matrix = Transform.m_matrix;
+
+ using var pixmap = m_image switch
+ {
+ Bitmap bm => bm.m_bitmap.PeekPixels(),
+ Svg svg => svg.InnerImage.PeekPixels(),
+ _ => throw new NotImplementedException($"image type {m_image.GetType().Name}.")
+ };
+
+ using var bitmap = new SKBitmap();
+ using var subset = pixmap.ExtractSubset(SKRectI.Round(m_bounds.m_rect));
+ bitmap.InstallPixels(subset);
+
+ using var surfece = SKSurface.Create(info);
+ surfece.Canvas.DrawBitmap(bitmap, 0, 0);
+
+ using var src = surfece.Snapshot();
+
+ m_paint.Shader = SKShader.CreateImage(src, tmx, tmy, matrix);
+ }
+
+ #endregion
+}
diff --git a/test/Common/BitmapUnitTest.cs b/test/Common/BitmapUnitTest.cs
index 614bc05..3ab5a20 100644
--- a/test/Common/BitmapUnitTest.cs
+++ b/test/Common/BitmapUnitTest.cs
@@ -1,6 +1,5 @@
using System;
using System.IO;
-using GeneXus.Drawing;
namespace GeneXus.Drawing.Test;
diff --git a/test/Common/ColorConverterUnitTests.cs b/test/Common/ColorConverterUnitTests.cs
new file mode 100644
index 0000000..5c3478c
--- /dev/null
+++ b/test/Common/ColorConverterUnitTests.cs
@@ -0,0 +1,100 @@
+namespace GeneXus.Drawing.Test;
+
+internal class ColorConverterTests
+{
+ private ColorConverter converter;
+
+ [SetUp]
+ public void SetUp()
+ {
+ converter = new ColorConverter();
+ }
+
+ [Test]
+ public void Method_CanConvertFrom_String()
+ {
+ Assert.That(converter.CanConvertFrom(typeof(string)), Is.True);
+ }
+
+ [Test]
+ public void Method_CanConvertFrom_Color()
+ {
+ Assert.That(converter.CanConvertFrom(typeof(Color)), Is.False);
+ }
+
+ [Test]
+ public void Method_CanConvertTo_String()
+ {
+ Assert.That(converter.CanConvertTo(typeof(string)), Is.True);
+ }
+
+ [Test]
+ public void Method_CanConvertTo_Int()
+ {
+ Assert.That(converter.CanConvertTo(typeof(int)), Is.False);
+ }
+
+ [Test]
+ public void Method_ConvertFrom_ColorName()
+ {
+ var result = converter.ConvertFrom("Red");
+
+ Assert.That(result, Is.EqualTo(Color.Red));
+ }
+
+ [Test]
+ public void Method_ConvertFrom_HexString()
+ {
+ var result = converter.ConvertFrom("#FF0000");
+
+ Assert.That(result, Is.EqualTo(Color.Red));
+ }
+
+ [Test]
+ public void Method_ConvertFrom_Empty()
+ {
+ var result = converter.ConvertFrom("");
+
+ Assert.That(result, Is.EqualTo(Color.Empty));
+ }
+
+ [Test]
+ public void Method_ConvertTo_String_Named()
+ {
+ var result = converter.ConvertTo(Color.Blue, typeof(string));
+
+ Assert.That(result, Is.EqualTo("Blue"));
+ }
+
+ [Test]
+ public void Method_ConvertTo_String_Empty()
+ {
+ var result = converter.ConvertTo(Color.Empty, typeof(string));
+
+ Assert.That(result, Is.EqualTo(string.Empty));
+ }
+
+ [Test]
+ public void Method_ConvertFromInvariantString_Named()
+ {
+ var result = converter.ConvertFromInvariantString("Green");
+
+ Assert.That(result, Is.EqualTo(Color.Green));
+ }
+
+ [Test]
+ public void Method_ConvertFromInvariantString_Hex()
+ {
+ var result = converter.ConvertFromInvariantString("#00FF00");
+
+ Assert.That(result, Is.EqualTo(Color.Lime));
+ }
+
+ [Test]
+ public void Method_ConvertFromInvariantString_Empty()
+ {
+ var result = converter.ConvertFromInvariantString("");
+
+ Assert.That(result, Is.EqualTo(Color.Empty));
+ }
+}
\ No newline at end of file
diff --git a/test/Common/ColorTranslatorUnitTest.cs b/test/Common/ColorTranslatorUnitTest.cs
new file mode 100644
index 0000000..0ee42ad
--- /dev/null
+++ b/test/Common/ColorTranslatorUnitTest.cs
@@ -0,0 +1,63 @@
+namespace GeneXus.Drawing.Test;
+
+internal class ColorTranslatorUnitTest
+{
+ [SetUp]
+ public void Setup()
+ {
+ }
+
+ [Test]
+ public void Method_ToHtml()
+ {
+ var color = Color.Red;
+ var htmlColor = ColorTranslator.ToHtml(color);
+
+ Assert.That(htmlColor, Is.EqualTo("Red"));
+ }
+
+ [Test]
+ public void Method_FromHtml()
+ {
+ var htmlColor = "#FF0000";
+ var color = ColorTranslator.FromHtml(htmlColor);
+
+ Assert.That(color, Is.EqualTo(Color.Red));
+ }
+
+ [Test]
+ public void Method_ToOle()
+ {
+ var color = Color.Blue;
+ var oleColor = ColorTranslator.ToOle(color);
+
+ Assert.That(oleColor, Is.EqualTo(0xFF0000));
+ }
+
+ [Test]
+ public void Method_FromOle()
+ {
+ var oleColor = 0xFF0000;
+ var color = ColorTranslator.FromOle(oleColor);
+
+ Assert.That(color, Is.EqualTo(Color.Blue));
+ }
+
+ [Test]
+ public void Method_ToWin32()
+ {
+ var color = Color.Lime;
+ var win32Color = ColorTranslator.ToWin32(color);
+
+ Assert.That(win32Color, Is.EqualTo(0x00FF00));
+ }
+
+ [Test]
+ public void Method_FromWin32()
+ {
+ var win32Color = 0x00FF00;
+ var color = ColorTranslator.FromWin32(win32Color);
+
+ Assert.That(color, Is.EqualTo(Color.Lime));
+ }
+}
\ No newline at end of file
diff --git a/test/Common/Drawing2D/GraphicsPathUnitTest.cs b/test/Common/Drawing2D/GraphicsPathUnitTest.cs
new file mode 100644
index 0000000..194823e
--- /dev/null
+++ b/test/Common/Drawing2D/GraphicsPathUnitTest.cs
@@ -0,0 +1,629 @@
+using GeneXus.Drawing.Drawing2D;
+using System;
+using System.Linq;
+
+namespace GeneXus.Drawing.Test.Drawing2D;
+
+internal class GraphicsPathUnitTest
+{
+ [SetUp]
+ public void Setup()
+ {
+ }
+
+ [Test]
+ public void Constructor_Default()
+ {
+ using var path = new GraphicsPath();
+ Assert.Multiple(() =>
+ {
+ Assert.That(path.FillMode, Is.EqualTo(FillMode.Alternate));
+ Assert.That(path.PointCount, Is.EqualTo(0));
+ Assert.That(path.PathPoints, Is.Empty);
+ Assert.That(path.PathTypes, Is.Empty);
+ });
+ }
+
+ [Test]
+ public void Constructor_FillMode()
+ {
+ var fillMode = FillMode.Winding;
+
+ using var path = new GraphicsPath(fillMode);
+ Assert.Multiple(() =>
+ {
+ Assert.That(path.FillMode, Is.EqualTo(fillMode));
+ Assert.That(path.PointCount, Is.EqualTo(0));
+ Assert.That(path.PathPoints, Is.Empty);
+ Assert.That(path.PathTypes, Is.Empty);
+ });
+ }
+
+ [Test]
+ public void Constructor_PointsAndTypes()
+ {
+ var fillMode = FillMode.Winding;
+ PointF[] points = { new(0, 0), new(1, 1) };
+ byte[] types = { (byte)PathPointType.Start, (byte)PathPointType.Line };
+
+ using var path = new GraphicsPath(points, types, fillMode);
+ Assert.Multiple(() =>
+ {
+ Assert.That(path.FillMode, Is.EqualTo(fillMode));
+ Assert.That(path.PointCount, Is.EqualTo(2));
+ Assert.That(path.PathPoints, Is.EqualTo(points));
+ Assert.That(path.PathTypes, Is.EqualTo(types));
+ });
+ }
+
+ [Test]
+ public void Property_FillMode()
+ {
+ using var path = new GraphicsPath();
+ path.FillMode = FillMode.Winding;
+ Assert.That(path.FillMode, Is.EqualTo(FillMode.Winding));
+ }
+
+ [Test]
+ public void Method_Clone()
+ {
+ var fillMode = FillMode.Winding;
+
+ using var path1 = new GraphicsPath(fillMode);
+ path1.AddLine(0, 0, 1, 1);
+
+ var path2 = path1.Clone();
+ Assert.Multiple(() =>
+ {
+ Assert.That(path2, Is.Not.Null);
+ Assert.That(path2, Is.Not.SameAs(path1));
+ Assert.That(path2, Is.TypeOf());
+
+ if (path2 is GraphicsPath cloned)
+ {
+ Assert.That(cloned.FillMode, Is.EqualTo(path1.FillMode));
+ Assert.That(cloned.PointCount, Is.EqualTo(path1.PointCount));
+ Assert.That(cloned.PathPoints, Is.EqualTo(path1.PathPoints));
+ Assert.That(cloned.PathTypes, Is.EqualTo(path1.PathTypes));
+
+ cloned.Dispose();
+ }
+ });
+ }
+
+ [Test]
+ public void Method_AddArc()
+ {
+ var rect = new RectangleF(0, 0, 100, 100);
+
+ using var path = new GraphicsPath();
+ path.AddArc(rect, 45, 90);
+ Assert.Multiple(() =>
+ {
+ // TODO: remove this assert and replace with below asserts when method has been fixed
+ Assert.That(path.PointCount, Is.GreaterThan(0));
+ /*Assert.That(path.PointCount, Is.EqualTo(4));
+ Assert.That(path.PathPoints, Is.EqualTo(new[]
+ {
+ new PointF(85.35534f, 85.35532f),
+ new PointF(65.82913f, 104.8815f),
+ new PointF(34.17089f, 104.8815f),
+ new PointF(14.64468f, 85.35535f)
+ }));
+ Assert.That(path.PathTypes, Is.EqualTo(new[]
+ {
+ (byte)PathPointType.Start,
+ (byte)PathPointType.Bezier,
+ (byte)PathPointType.Bezier,
+ (byte)PathPointType.Bezier
+ }));*/
+ });
+ }
+
+ [Test]
+ public void Method_AddBezier()
+ {
+ var points = new[] { new PointF(1, 5), new PointF(1, 1), new PointF(2, 1), new PointF(3, 0) };
+
+ using var path = new GraphicsPath();
+ path.AddBezier(points[0], points[1], points[2], points[3]);
+ Assert.Multiple(() =>
+ {
+ Assert.That(path.PointCount, Is.EqualTo(4));
+ Assert.That(path.PathPoints, Is.EqualTo(points));
+ Assert.That(path.PathTypes, Is.EqualTo(new []
+ {
+ (byte)PathPointType.Start,
+ (byte)PathPointType.Bezier,
+ (byte)PathPointType.Bezier,
+ (byte)PathPointType.Bezier
+ }));
+ });
+ }
+
+ [Test]
+ public void Method_AddClosedCurve()
+ {
+ var points = new[] { new PointF(0, 0), new PointF(1, 1), new PointF(2, 0) };
+
+ using var path = new GraphicsPath();
+ path.AddClosedCurve(points);
+ Assert.Multiple(() =>
+ {
+ Assert.That(path.PointCount, Is.EqualTo(10));
+ Assert.That(path.PathPoints, Is.EqualTo(new[]
+ {
+ new PointF(0, 0),
+ new PointF(-0.16666667f, 0.16666667f),
+ new PointF(0.6666666f, 1),
+ new PointF(1, 1),
+ new PointF(1.3333334f, 1),
+ new PointF(2.1666667f, 0.16666667f),
+ new PointF(2, 0),
+ new PointF(1.8333334f, -0.16666667f),
+ new PointF(0.16666667f, -0.16666667f),
+ new PointF(0, 0)
+ }));
+ Assert.That(path.PathTypes, Is.EqualTo(new[]
+ {
+ (byte)PathPointType.Start,
+ (byte)PathPointType.Bezier,
+ (byte)PathPointType.Bezier,
+ (byte)PathPointType.Bezier,
+ (byte)PathPointType.Bezier,
+ (byte)PathPointType.Bezier,
+ (byte)PathPointType.Bezier,
+ (byte)PathPointType.Bezier,
+ (byte)PathPointType.Bezier,
+ (byte)PathPointType.Bezier + (byte)PathPointType.CloseSubpath
+ }));
+ });
+ }
+
+ [Test]
+ public void Method_AddCurve()
+ {
+ var points = new[] { new PointF(0, 0), new PointF(1, 1), new PointF(2, 0) };
+
+ using var path = new GraphicsPath();
+ path.AddCurve(points);
+ Assert.Multiple(() =>
+ {
+ Assert.That(path.PointCount, Is.EqualTo(7));
+ Assert.That(path.PathPoints, Is.EqualTo(new[]
+ {
+ new PointF(0, 0),
+ new PointF(0.16666667f, 0.16666667f),
+ new PointF(0.6666666f, 1),
+ new PointF(1, 1),
+ new PointF(1.3333334f, 1),
+ new PointF(1.8333334f, 0.16666667f),
+ new PointF(2, 0)
+ }));
+ Assert.That(path.PathTypes, Is.EqualTo(new[]
+ {
+ (byte)PathPointType.Start,
+ (byte)PathPointType.Bezier,
+ (byte)PathPointType.Bezier,
+ (byte)PathPointType.Bezier,
+ (byte)PathPointType.Bezier,
+ (byte)PathPointType.Bezier,
+ (byte)PathPointType.Bezier
+ }));
+ });
+ }
+
+ [Test]
+ public void Method_AddEllipse()
+ {
+ var rect = new RectangleF(0, 0, 100, 50);
+
+ using var path = new GraphicsPath();
+ path.AddEllipse(rect);
+ Assert.Multiple(() =>
+ {
+ // TODO: remove this assert and replace with below asserts when method has been fixed
+ Assert.That(path.PointCount, Is.GreaterThan(0));
+ /*Assert.That(path.PointCount, Is.EqualTo(13));
+ Assert.That(path.PathPoints, Is.EqualTo(new[]
+ {
+ new PointF(100, 25),
+ new PointF(100, 38.80712f),
+ new PointF(77.61423f, 50),
+ new PointF(50, 50),
+ new PointF(22.38576f, 50),
+ new PointF(0, 38.80712f),
+ new PointF(0, 25),
+ new PointF(0, 11.19288f),
+ new PointF(22.38576f, 0),
+ new PointF(50, 0),
+ new PointF(77.61423f, 0),
+ new PointF(100, 11.19288f),
+ new PointF(100, 25)
+ }));
+ Assert.That(path.PathTypes, Is.EqualTo(new[]
+ {
+ (byte)PathPointType.Start,
+ (byte)PathPointType.Bezier,
+ (byte)PathPointType.Bezier,
+ (byte)PathPointType.Bezier,
+ (byte)PathPointType.Bezier,
+ (byte)PathPointType.Bezier,
+ (byte)PathPointType.Bezier,
+ (byte)PathPointType.Bezier,
+ (byte)PathPointType.Bezier,
+ (byte)PathPointType.Bezier,
+ (byte)PathPointType.Bezier,
+ (byte)PathPointType.Bezier,
+ (byte)PathPointType.Bezier | (byte)PathPointType.CloseSubpath
+ }));*/
+ });
+ }
+
+ [Test]
+ public void Method_AddLine()
+ {
+ var pt1 = new PointF(0, 0);
+ var pt2 = new PointF(1, 1);
+
+ using var path = new GraphicsPath();
+ path.AddLine(pt1, pt2);
+ Assert.Multiple(() =>
+ {
+ Assert.That(path.PointCount, Is.EqualTo(2));
+ Assert.That(path.PathPoints, Is.EqualTo(new[]
+ {
+ new PointF(0, 0),
+ new PointF(1, 1)
+ }));
+ Assert.That(path.PathTypes, Is.EqualTo(new[]
+ {
+ (byte)PathPointType.Start,
+ (byte)PathPointType.Line
+ }));
+ });
+ }
+
+ [Test]
+ public void Method_AddPie()
+ {
+ var rect = new RectangleF(0, 0, 100, 50);
+
+ using var path = new GraphicsPath();
+ path.AddPie(rect, 0, 90);
+ Assert.Multiple(() =>
+ {
+ // TODO: remove this assert and replace with below asserts when method has been fixed
+ Assert.That(path.PointCount, Is.GreaterThan(0));
+ /*Assert.That(path.PointCount, Is.EqualTo(5));
+ Assert.That(path.PathPoints, Is.EqualTo(new[]
+ {
+ new PointF(50, 25),
+ new PointF(99.99999f, 25),
+ new PointF(99.99999f, 38.80711f),
+ new PointF(77.61423f, 49.99999f),
+ new PointF(50, 50)
+ }));
+ Assert.That(path.PathTypes, Is.EqualTo(new[]
+ {
+ (byte)PathPointType.Start,
+ (byte)PathPointType.Line,
+ (byte)PathPointType.Bezier,
+ (byte)PathPointType.Bezier,
+ (byte)PathPointType.Bezier + (byte)PathPointType.CloseSubpath
+ }));*/
+ });
+ }
+
+ [Test]
+ public void Method_AddPolygon()
+ {
+ var points = new PointF[] { new(0, 0), new(1, 1), new(2, 0) };
+
+ using var path = new GraphicsPath();
+ path.AddPolygon(points);
+ Assert.Multiple(() =>
+ {
+ Assert.That(path.PointCount, Is.EqualTo(3));
+ Assert.That(path.PathPoints, Is.EqualTo(new[]
+ {
+ new PointF(0, 0),
+ new PointF(1, 1),
+ new PointF(2, 0)
+ }));
+ Assert.That(path.PathTypes, Is.EqualTo(new[]
+ {
+ (byte)PathPointType.Start,
+ (byte)PathPointType.Line,
+ (byte)PathPointType.Line | (byte)PathPointType.CloseSubpath
+ }));
+ });
+ }
+
+ [Test]
+ public void Method_AddRectangle()
+ {
+ var rect = new RectangleF(0, 0, 100, 50);
+
+ using var path = new GraphicsPath();
+ path.AddRectangle(rect);
+ Assert.Multiple(() =>
+ {
+ Assert.That(path.PointCount, Is.EqualTo(4));
+ Assert.That(path.PathPoints, Is.EqualTo(new[]
+ {
+ new PointF(0, 0),
+ new PointF(100, 0),
+ new PointF(100, 50),
+ new PointF(0, 50)
+ }));
+ Assert.That(path.PathTypes, Is.EqualTo(new[]
+ {
+ (byte)PathPointType.Start,
+ (byte)PathPointType.Line,
+ (byte)PathPointType.Line,
+ (byte)PathPointType.Line | (byte)PathPointType.CloseSubpath
+ }));
+ });
+ }
+
+ [Test]
+ public void Method_AddString()
+ {
+ var text = "x!";
+ var font = new Font("Arial", 16);
+ var format = new StringFormat();
+ var origin = new PointF(0, 0);
+ var size = new SizeF(12, 12);
+ var layout = new RectangleF(origin, size);
+
+ using var path = new GraphicsPath();
+ path.AddString(text, font.FontFamily, (int)font.Style, font.Size, origin, format);
+ Assert.Multiple(() =>
+ {
+ // TODO: remove this assert and replace with below asserts when method has been fixed
+ Assert.That(path.PointCount, Is.GreaterThan(0));
+ /*Assert.That(path.PointCount, Is.EqualTo(29));
+ Assert.That(path.PathPoints, Is.EqualTo(new[]
+ {
+ new PointF(2.78125f, 14.48438f),
+ new PointF(5.8125f, 10.17188f),
+ new PointF(3.007813f, 6.1875f),
+ new PointF(4.765625f, 6.1875f),
+ new PointF(6.039063f, 8.132813f),
+ new PointF(6.278646f, 8.502604f),
+ new PointF(6.471354f, 8.8125f),
+ new PointF(6.617188f, 9.0625f),
+ new PointF(6.846354f, 8.71875f),
+ new PointF(7.057292f, 8.414063f),
+ new PointF(7.25f, 8.148438f),
+ new PointF(8.648438f, 6.1875f),
+ new PointF(10.32813f, 6.1875f),
+ new PointF(7.460938f, 10.09375f),
+ new PointF(10.54688f, 14.48438f),
+ new PointF(8.820313f, 14.48438f),
+ new PointF(7.117188f, 11.90625f),
+ new PointF(6.664063f, 11.21094f),
+ new PointF(4.484375f, 14.48438f),
+ new PointF(12.71094f, 11.64063f),
+ new PointF(12.28125f, 5.570313f),
+ new PointF(12.28125f, 3.03125f),
+ new PointF(14.02344f, 3.03125f),
+ new PointF(14.02344f, 5.570313f),
+ new PointF(13.61719f, 11.64063f),
+ new PointF(12.34375f, 14.48438f),
+ new PointF(12.34375f, 12.88281f),
+ new PointF(13.96094f, 12.88281f),
+ new PointF(13.96094f, 14.48438f)
+ }));
+ Assert.That(path.PathTypes, Is.EqualTo(new[]
+ {
+ (byte)PathPointType.Start,
+ (byte)PathPointType.Line,
+ (byte)PathPointType.Line,
+ (byte)PathPointType.Line,
+ (byte)PathPointType.Line,
+ (byte)PathPointType.Bezier,
+ (byte)PathPointType.Bezier,
+ (byte)PathPointType.Bezier,
+ (byte)PathPointType.Bezier,
+ (byte)PathPointType.Bezier,
+ (byte)PathPointType.Bezier,
+ (byte)PathPointType.Line,
+ (byte)PathPointType.Line,
+ (byte)PathPointType.Line,
+ (byte)PathPointType.Line,
+ (byte)PathPointType.Line,
+ (byte)PathPointType.Line,
+ (byte)PathPointType.Line,
+ (byte)PathPointType.Line | (byte)PathPointType.DashMode | (byte)PathPointType.CloseSubpath,
+ (byte)PathPointType.Start,
+ (byte)PathPointType.Line,
+ (byte)PathPointType.Line,
+ (byte)PathPointType.Line,
+ (byte)PathPointType.Line,
+ (byte)PathPointType.Line + (byte)PathPointType.CloseSubpath,
+ (byte)PathPointType.Start,
+ (byte)PathPointType.Line,
+ (byte)PathPointType.Line,
+ (byte)PathPointType.Line + (byte)PathPointType.DashMode + (byte)PathPointType.CloseSubpath
+ }));*/
+ });
+ }
+
+ [Test]
+ public void Method_CloseFigure()
+ {
+ var points = new[] { new PointF(0, 0), new PointF(1, 1), new PointF(2, 0) };
+
+ using var path = new GraphicsPath();
+ path.AddCurve(points);
+
+ path.CloseFigure();
+ Assert.That((path.PathTypes[path.PointCount - 1] & (byte)PathPointType.CloseSubpath) == (byte)PathPointType.CloseSubpath);
+ }
+
+ [Test]
+ public void Method_Flatten()
+ {
+ var points = new[] { new PointF(1, 5), new PointF(1, 3), new PointF(5, 5), new PointF(5, 3) };
+
+ var matrix = new Matrix();
+ matrix.Translate(5, 10);
+
+ using var path = new GraphicsPath();
+ path.AddBezier(points[0], points[1], points[2], points[3]);
+
+ path.Flatten(matrix, 0.25f);
+ Assert.Multiple(() =>
+ {
+ Assert.That(path.PointCount, Is.EqualTo(5));
+ Assert.That(path.PathPoints, Is.EqualTo(new[]
+ {
+ new PointF(6f, 15f),
+ new PointF(6.625f, 14.125f),
+ new PointF(8f, 14f),
+ new PointF(9.375f, 13.875f),
+ new PointF(10f, 13f)
+ }));
+ Assert.That(path.PathTypes, Is.EqualTo(new[]
+ {
+ (byte)PathPointType.Start,
+ (byte)PathPointType.Line,
+ (byte)PathPointType.Line,
+ (byte)PathPointType.Line,
+ (byte)PathPointType.Line
+ }));
+ });
+ }
+
+ [Test]
+ public void Method_GetBounds()
+ {
+ var rect = new RectangleF(0, 0, 100, 50);
+
+ using var path = new GraphicsPath();
+ path.AddRectangle(rect);
+
+ var bounds = path.GetBounds();
+ Assert.Multiple(() =>
+ {
+ Assert.That(bounds.X, Is.EqualTo(0));
+ Assert.That(bounds.Y, Is.EqualTo(0));
+ Assert.That(bounds.Width, Is.EqualTo(100));
+ Assert.That(bounds.Height, Is.EqualTo(50));
+ });
+ }
+
+ [Test]
+ public void Method_GetLastPoint()
+ {
+ var points = new[] { new PointF(0, 0), new PointF(1, 1), new PointF(2, 0) };
+
+ using var path = new GraphicsPath();
+ path.AddCurve(points);
+
+ Assert.That(path.GetLastPoint(), Is.EqualTo(points[points.Length - 1]));
+ }
+
+ [Test]
+ public void Method_IsOutlineVisible()
+ {
+ var pen = new Pen(Color.Black, 1);
+ var rect = new RectangleF(0, 0, 3, 3);
+
+ using var path = new GraphicsPath();
+ path.AddRectangle(rect);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(path.IsOutlineVisible(0, 0, pen), Is.True);
+ Assert.That(path.IsOutlineVisible(2, 2, pen), Is.False);
+ Assert.That(path.IsOutlineVisible(3, 3, pen), Is.True);
+ Assert.That(path.IsOutlineVisible(4, 4, pen), Is.False);
+ });
+ }
+
+ [Test]
+ public void Method_IsVisible()
+ {
+ var rect = new RectangleF(0, 0, 3, 3);
+
+ using var path = new GraphicsPath();
+ path.AddRectangle(rect);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(path.IsVisible(0, 0), Is.True);
+ Assert.That(path.IsVisible(2, 2), Is.True);
+ Assert.That(path.IsVisible(3, 3), Is.False);
+ Assert.That(path.IsVisible(4, 4), Is.False);
+ });
+ }
+
+ [Test]
+ public void Method_Reverse()
+ {
+ var rect = new RectangleF(0, 0, 100, 50);
+
+ using var path = new GraphicsPath();
+ path.AddLine(0, 0, 1, 1);
+ path.AddLine(1, 1, 2, 2);
+ path.AddBezier(2, 2, 2, 3, 3, 3, 3, 4);
+ path.AddRectangle(rect);
+
+ var reversedPoints = path.PathPoints.Reverse().ToArray();
+
+ path.Reverse();
+ Assert.Multiple(() =>
+ {
+ Assert.That(path.PathPoints, Is.EqualTo(reversedPoints));
+ Assert.That(path.PathTypes, Is.EqualTo(new[]
+ {
+ (byte)PathPointType.Start,
+ (byte)PathPointType.Line,
+ (byte)PathPointType.Line,
+ (byte)PathPointType.Line | (byte)PathPointType.CloseSubpath,
+ (byte)PathPointType.Start,
+ (byte)PathPointType.Bezier,
+ (byte)PathPointType.Bezier,
+ (byte)PathPointType.Bezier,
+ (byte)PathPointType.Line,
+ (byte)PathPointType.Line
+ }));
+ });
+ }
+
+ [Test]
+ public void Method_Reset()
+ {
+ var rect = new RectangleF(0, 0, 100, 50);
+
+ using var path = new GraphicsPath();
+ path.AddRectangle(rect);
+
+ path.Reset();
+ Assert.Multiple(() =>
+ {
+ Assert.That(path.PointCount, Is.EqualTo(0));
+ Assert.That(path.PathPoints, Is.Empty);
+ Assert.That(path.PathTypes, Is.Empty);
+ });
+ }
+
+ [Test]
+ public void Method_Transform()
+ {
+ using var path = new GraphicsPath();
+ path.AddLine(0, 0, 1, 1);
+
+ using var matrix = new Matrix();
+ matrix.Translate(1, 1);
+
+ var transformedPoints = path.PathPoints;
+ matrix.TransformPoints(transformedPoints);
+
+ path.Transform(matrix);
+ Assert.That(path.PathPoints, Is.EqualTo(transformedPoints));
+ }
+}
\ No newline at end of file
diff --git a/test/Common/Drawing2D/HatchBrushUnitTest.cs b/test/Common/Drawing2D/HatchBrushUnitTest.cs
new file mode 100644
index 0000000..502220f
--- /dev/null
+++ b/test/Common/Drawing2D/HatchBrushUnitTest.cs
@@ -0,0 +1,105 @@
+using System.IO;
+using GeneXus.Drawing.Drawing2D;
+
+namespace GeneXus.Drawing.Test.Drawing2D;
+
+internal class HatchBrushUnitTest
+{
+ [SetUp]
+ public void Setup()
+ {
+ }
+
+ [Test]
+ [TestCase(HatchStyle.Horizontal)]
+ [TestCase(HatchStyle.Vertical)]
+ [TestCase(HatchStyle.ForwardDiagonal)]
+ [TestCase(HatchStyle.BackwardDiagonal)]
+ [TestCase(HatchStyle.Cross)]
+ [TestCase(HatchStyle.DiagonalCross)]
+ [TestCase(HatchStyle.Percent05)]
+ [TestCase(HatchStyle.Percent10)]
+ [TestCase(HatchStyle.Percent20)]
+ [TestCase(HatchStyle.Percent25)]
+ [TestCase(HatchStyle.Percent30)]
+ [TestCase(HatchStyle.Percent40)]
+ [TestCase(HatchStyle.Percent50)]
+ [TestCase(HatchStyle.Percent60)]
+ [TestCase(HatchStyle.Percent70)]
+ [TestCase(HatchStyle.Percent75)]
+ [TestCase(HatchStyle.Percent80)]
+ [TestCase(HatchStyle.Percent90)]
+ [TestCase(HatchStyle.LightDownwardDiagonal)]
+ [TestCase(HatchStyle.LightUpwardDiagonal)]
+ [TestCase(HatchStyle.DarkDownwardDiagonal)]
+ [TestCase(HatchStyle.DarkUpwardDiagonal)]
+ [TestCase(HatchStyle.WideDownwardDiagonal)]
+ [TestCase(HatchStyle.WideUpwardDiagonal)]
+ [TestCase(HatchStyle.LightVertical)]
+ [TestCase(HatchStyle.LightHorizontal)]
+ [TestCase(HatchStyle.NarrowVertical)]
+ [TestCase(HatchStyle.NarrowHorizontal)]
+ [TestCase(HatchStyle.DarkVertical)]
+ [TestCase(HatchStyle.DarkHorizontal)]
+ [TestCase(HatchStyle.DashedDownwardDiagonal)]
+ [TestCase(HatchStyle.DashedUpwardDiagonal)]
+ [TestCase(HatchStyle.DashedHorizontal)]
+ [TestCase(HatchStyle.DashedVertical)]
+ [TestCase(HatchStyle.SmallConfetti)]
+ [TestCase(HatchStyle.LargeConfetti)]
+ [TestCase(HatchStyle.ZigZag)]
+ [TestCase(HatchStyle.Wave)]
+ [TestCase(HatchStyle.DiagonalBrick)]
+ [TestCase(HatchStyle.HorizontalBrick)]
+ [TestCase(HatchStyle.Weave)]
+ [TestCase(HatchStyle.Plaid)]
+ [TestCase(HatchStyle.Divot)]
+ [TestCase(HatchStyle.DottedGrid)]
+ [TestCase(HatchStyle.DottedDiamond)]
+ [TestCase(HatchStyle.Shingle)]
+ [TestCase(HatchStyle.Trellis)]
+ [TestCase(HatchStyle.Sphere)]
+ [TestCase(HatchStyle.SmallGrid)]
+ [TestCase(HatchStyle.SmallCheckerBoard)]
+ [TestCase(HatchStyle.LargeCheckerBoard)]
+ [TestCase(HatchStyle.OutlinedDiamond)]
+ [TestCase(HatchStyle.SolidDiamond)]
+ public void Constructor_StyleForeBack(HatchStyle style)
+ {
+ var fore = Color.Red;
+ var back = Color.Blue;
+
+ using var brush = new HatchBrush(style, fore, back);
+ Assert.Multiple(() =>
+ {
+ Assert.That(brush.BackgroundColor, Is.EqualTo(back));
+ Assert.That(brush.ForegroundColor, Is.EqualTo(fore));
+ Assert.That(brush.HatchStyle, Is.EqualTo(style));
+
+ string name = style switch
+ {
+ HatchStyle.Cross => "Cross", // defines LargeGrid and Max aliases
+ HatchStyle.Horizontal => "Horizontal", // defines Min alias
+ _ => style.ToString()
+ };
+ string path = Path.Combine("brush", "hatch", $"Style{name}.png");
+ float similarity = Utils.CompareImage(path, brush, true);
+ Assert.That(similarity, Is.GreaterThan(0.9));
+ });
+ }
+
+ [Test]
+ public void Method_Clone()
+ {
+ using var brush1 = new HatchBrush(HatchStyle.Vertical, Color.Red, Color.Blue);
+ using var brush2 = brush1.Clone() as HatchBrush;
+ Assert.Multiple(() =>
+ {
+ Assert.That(brush2, Is.Not.Null);
+ Assert.That(brush2, Is.Not.SameAs(brush1));
+ Assert.That(brush2.HatchStyle, Is.EqualTo(brush1.HatchStyle));
+ Assert.That(brush2.ForegroundColor, Is.EqualTo(brush1.ForegroundColor));
+ Assert.That(brush2.BackgroundColor, Is.EqualTo(brush1.BackgroundColor));
+ });
+ }
+}
\ No newline at end of file
diff --git a/test/Common/Drawing2D/LinearGradientBrushUnitTest.cs b/test/Common/Drawing2D/LinearGradientBrushUnitTest.cs
new file mode 100644
index 0000000..dd09fa6
--- /dev/null
+++ b/test/Common/Drawing2D/LinearGradientBrushUnitTest.cs
@@ -0,0 +1,242 @@
+using System;
+using System.IO;
+using GeneXus.Drawing.Drawing2D;
+
+namespace GeneXus.Drawing.Test.Drawing2D;
+
+internal class LinearGradientBrushUnitTest
+{
+ [SetUp]
+ public void Setup()
+ {
+ }
+
+ [Test]
+ public void Constructor_Points()
+ {
+ var startPoint = new Point(0, 0);
+ var endPoint = new Point(1, 1);
+
+ var bounds = new RectangleF(startPoint.X, startPoint.Y, startPoint.X + endPoint.X, startPoint.Y + endPoint.Y);
+
+ var startColor = Color.Red;
+ var endColor = Color.Blue;
+
+ using var brush = new LinearGradientBrush(startPoint, endPoint, startColor, endColor);
+ Assert.Multiple(() =>
+ {
+ Assert.That(brush.LinearColors, Is.EqualTo(new[] { startColor, endColor }));
+ Assert.That(brush.Rectangle, Is.EqualTo(bounds));
+ Assert.That(brush.WrapMode, Is.EqualTo(WrapMode.Tile));
+ });
+ }
+
+ [Test]
+ [TestCase(LinearGradientMode.Horizontal, new[] { 0.9999999f, 0f, 0f, 0.9999999f, 0f, 0f })]
+ [TestCase(LinearGradientMode.Vertical, new[] { -1.907349E-07f, 0.9999999f, -1f, -4.768372E-08f, 50f, 7.152557E-07f })]
+ [TestCase(LinearGradientMode.BackwardDiagonal, new[] { -1f, 0.9999998f, -1f, -1f, 74.99999f, 25f })]
+ [TestCase(LinearGradientMode.ForwardDiagonal, new[] { 0.9999999f, 0.9999999f, -1f, 0.9999999f, 25f, -25f })]
+ public void Constructor_RectangleMode(LinearGradientMode mode, float[] elements)
+ {
+ var rect = new RectangleF(15, 15, 20, 20);
+
+ var startColor = Color.Red;
+ var endColor = Color.Blue;
+
+ using var brush = new LinearGradientBrush(rect, startColor, endColor, mode);
+ Assert.Multiple(() =>
+ {
+ Assert.That(brush.LinearColors, Is.EqualTo(new[] { startColor, endColor }));
+ Assert.That(brush.Transform.Elements, Is.EqualTo(elements).Within(1e-05));
+ Assert.That(brush.Rectangle, Is.EqualTo(rect));
+ Assert.That(brush.WrapMode, Is.EqualTo(WrapMode.Tile));
+
+ string path = Path.Combine("brush", "linear", $"Mode{mode}.png");
+ float similarity = Utils.CompareImage(path, brush, true);
+ Assert.That(similarity, Is.GreaterThan(0.9));
+ });
+ }
+
+ [Test]
+ [TestCase(0f, new[] { 0.9999999f, 0f, 0f, 0.9999999f, 0f, 0f })]
+ [TestCase(45f, new[] { 0.999999f, 0.9999999f, -1f, 0.9999999f, 25f, -25f })]
+ [TestCase(75f, new[] { 0.3169872f, 1.183012f, -1.183013f, 0.3169872f, 46.65063f, -12.5f })]
+ [TestCase(90f, new[] { -1.907349E-07f, 0.9999999f, -1f, -4.768372E-08f, 50f, 7.152557E-07f })]
+ public void Constructor_RectangleAngle(float angle, float[] elements)
+ {
+ var rect = new RectangleF(15, 15, 20, 20);
+
+ var startColor = Color.Red;
+ var endColor = Color.Blue;
+
+ using var brush = new LinearGradientBrush(rect, startColor, endColor, angle);
+ Assert.Multiple(() =>
+ {
+ Assert.That(brush.LinearColors, Is.EqualTo(new[] { startColor, endColor }));
+ Assert.That(brush.Transform.Elements, Is.EqualTo(elements).Within(1e-05));
+ Assert.That(brush.Rectangle, Is.EqualTo(rect));
+ Assert.That(brush.WrapMode, Is.EqualTo(WrapMode.Tile));
+
+ string path = Path.Combine("brush", "linear", $"Angle{angle}.png");
+ float similarity = Utils.CompareImage(path, brush, true);
+ Assert.That(similarity, Is.GreaterThan(0.9));
+ });
+ }
+
+ [Test]
+ public void Method_Clone()
+ {
+ using var brush1 = new LinearGradientBrush(new Point(0, 0), new Point(1, 1), Color.Red, Color.Blue);
+ using var brush2 = brush1.Clone() as LinearGradientBrush;
+ Assert.Multiple(() =>
+ {
+ Assert.That(brush2, Is.Not.Null);
+ Assert.That(brush2, Is.Not.SameAs(brush1));
+ Assert.That(brush2.LinearColors, Is.EqualTo(brush1.LinearColors));
+ Assert.That(brush2.Transform.Elements, Is.EqualTo(brush1.Transform.Elements));
+ Assert.That(brush2.Rectangle, Is.EqualTo(brush1.Rectangle));
+ Assert.That(brush2.WrapMode, Is.EqualTo(brush1.WrapMode));
+ });
+ }
+
+ [Test]
+ public void Property_Blend()
+ {
+ var bounds = new RectangleF(0, 0, 1, 1);
+ var colors = new Color[] { Color.Red, Color.Blue };
+
+ var blend = new Blend(4);
+ Array.Copy(new[] { 0f, 0.25f, 0.75f, 1f }, blend.Positions, 4);
+ Array.Copy(new[] { 0f, 0.25f, 0.25f, 0f }, blend.Factors, 4);
+
+ using var brush = new LinearGradientBrush(bounds, colors[0], colors[1], 0) { Blend = blend };
+ Assert.Multiple(() =>
+ {
+ Assert.That(brush.Blend.Factors, Is.EqualTo(blend.Factors));
+ Assert.That(brush.Blend.Positions, Is.EqualTo(blend.Positions));
+ Assert.That(brush.LinearColors, Is.EqualTo(colors));
+ Assert.That(brush.Rectangle, Is.EqualTo(bounds));
+ Assert.That(brush.WrapMode, Is.EqualTo(WrapMode.Tile));
+ });
+ }
+
+ [Test]
+ public void Property_Interpolation_And_Gamma()
+ {
+ var bounds = new RectangleF(0, 0, 1, 1);
+ var colors = new Color[] { Color.Red, Color.Blue };
+
+ var interpolation = new ColorBlend(3);
+ Array.Copy(new[] { 0f, 0.5f, 1f }, interpolation.Positions, 3);
+ Array.Copy(new[] { Color.Cyan, Color.Yellow, Color.Magenta }, interpolation.Colors, 3);
+
+ var gammaCorrection = true;
+
+ using var brush = new LinearGradientBrush(bounds, colors[0], colors[1], 0) { InterpolationColors = interpolation, GammaCorrection = gammaCorrection };
+ Assert.Multiple(() =>
+ {
+ Assert.That(brush.InterpolationColors.Colors, Is.EqualTo(interpolation.Colors));
+ Assert.That(brush.InterpolationColors.Positions, Is.EqualTo(interpolation.Positions));
+ Assert.That(brush.GammaCorrection, Is.EqualTo(gammaCorrection));
+ Assert.That(brush.LinearColors, Is.EqualTo(interpolation.Colors)); // NOTE: interpolation overrides linear colors
+ Assert.That(brush.Rectangle, Is.EqualTo(bounds));
+ Assert.That(brush.WrapMode, Is.EqualTo(WrapMode.Tile));
+ });
+ }
+
+ [Test]
+ public void Property_Transform()
+ {
+ var bounds = new RectangleF(0, 0, 1, 1);
+ var colors = new Color[] { Color.Red, Color.Blue };
+ var matrix = new Matrix(1, 2, 3, 4, 5, 6);
+
+ using var brush = new LinearGradientBrush(bounds, colors[0], colors[1], 0) { Transform = matrix };
+ Assert.Multiple(() =>
+ {
+ Assert.That(brush.LinearColors, Is.EqualTo(colors));
+ Assert.That(brush.Rectangle, Is.EqualTo(bounds));
+ Assert.That(brush.Transform, Is.EqualTo(matrix));
+ Assert.That(brush.WrapMode, Is.EqualTo(WrapMode.Tile));
+ });
+ }
+
+ [Test]
+ public void Property_WrapMode()
+ {
+ var bounds = new RectangleF(0, 0, 1, 1);
+ var colors = new Color[] { Color.Red, Color.Blue };
+ var wrapMode = WrapMode.Clamp;
+
+ using var brush = new LinearGradientBrush(bounds, colors[0], colors[1], 0) { WrapMode = wrapMode };
+ Assert.Multiple(() =>
+ {
+ Assert.That(brush.LinearColors, Is.EqualTo(colors));
+ Assert.That(brush.Rectangle, Is.EqualTo(bounds));
+ Assert.That(brush.WrapMode, Is.EqualTo(wrapMode));
+ });
+ }
+
+ [Test]
+ public void Method_SetSigmaBellShape()
+ {
+ var startPoint = new Point(0, 0);
+ var endPoint = new Point(1, 1);
+
+ var startColor = Color.Red;
+ var endColor = Color.Blue;
+
+ var (focus, scale) = (0.25f, 0.75f);
+
+ using var brush = new LinearGradientBrush(startPoint, endPoint, startColor, endColor);
+ brush.SetSigmaBellShape(focus, scale);
+ Assert.Multiple(() =>
+ {
+ var threshold = 1e-3f;
+ var length = brush.Blend.Factors.Length;
+ var mid = length / 2;
+
+ Assert.That(length, Is.EqualTo(511));
+
+ Assert.That(brush.Blend.Factors[0], Is.EqualTo(0));
+ Assert.That(brush.Blend.Positions[0], Is.EqualTo(0));
+
+ Assert.That(brush.Blend.Factors[1], Is.EqualTo(0.000675).Within(threshold));
+ Assert.That(brush.Blend.Positions[1], Is.EqualTo(0.000980).Within(threshold));
+
+ Assert.That(brush.Blend.Factors[mid], Is.EqualTo(scale));
+ Assert.That(brush.Blend.Positions[mid], Is.EqualTo(focus));
+
+ Assert.That(brush.Blend.Factors[length - 2], Is.EqualTo(0.000675).Within(threshold));
+ Assert.That(brush.Blend.Positions[length - 2], Is.EqualTo(0.997058).Within(threshold));
+
+ Assert.That(brush.Blend.Factors[length - 1], Is.EqualTo(0));
+ Assert.That(brush.Blend.Positions[length - 1], Is.EqualTo(1));
+ });
+ }
+
+ [Test]
+ public void Method_SetBlendTriangularShape()
+ {
+ var startPoint = new Point(0, 0);
+ var endPoint = new Point(1, 1);
+
+ var startColor = Color.Red;
+ var endColor = Color.Blue;
+
+ var (focus, scale) = (0.25f, 0.75f);
+
+ using var brush = new LinearGradientBrush(startPoint, endPoint, startColor, endColor);
+ brush.SetBlendTriangularShape(focus, scale);
+ Assert.Multiple(() =>
+ {
+ Assert.That(brush.Blend.Factors.Length, Is.EqualTo(3));
+ Assert.That(brush.Blend.Positions[0], Is.EqualTo(0));
+ Assert.That(brush.Blend.Factors[0], Is.EqualTo(0));
+ Assert.That(brush.Blend.Positions[1], Is.EqualTo(focus));
+ Assert.That(brush.Blend.Factors[1], Is.EqualTo(scale));
+ Assert.That(brush.Blend.Positions[2], Is.EqualTo(1));
+ Assert.That(brush.Blend.Factors[2], Is.EqualTo(0));
+ });
+ }
+}
\ No newline at end of file
diff --git a/test/Common/Drawing2D/MatrixUnitTest.cs b/test/Common/Drawing2D/MatrixUnitTest.cs
new file mode 100644
index 0000000..6ec04e1
--- /dev/null
+++ b/test/Common/Drawing2D/MatrixUnitTest.cs
@@ -0,0 +1,207 @@
+using GeneXus.Drawing.Drawing2D;
+using System.Numerics;
+
+namespace GeneXus.Drawing.Test.Drawing2D;
+
+internal class MatrixTests
+{
+ private const float TOLERANCE = 0.001f;
+
+ [SetUp]
+ public void Setup()
+ {
+ }
+
+ [Test]
+ public void Constructor_Default()
+ {
+ var matrix = new Matrix();
+ Assert.Multiple(() =>
+ {
+ Assert.That(matrix.IsIdentity, Is.True);
+ Assert.That(matrix.IsInvertible, Is.True);
+ Assert.That(matrix.Elements, Is.EquivalentTo(new float[] { 1, 0, 0, 1, 0, 0 }));
+ });
+ }
+
+ [Test]
+ public void Constructor_Elements()
+ {
+ var matrix = new Matrix(1, 2, 3, 4, 5, 6);
+ Assert.Multiple(() =>
+ {
+ Assert.That(matrix.IsIdentity, Is.False);
+ Assert.That(matrix.IsInvertible, Is.True);
+ Assert.That(matrix.Elements, Is.EqualTo(new float[] { 1, 2, 3, 4, 5, 6 }));
+ });
+ }
+
+ [Test]
+ public void Constructor_Matrix3x2()
+ {
+ var matrix3x2 = new Matrix3x2(1.0f, 0.0f, 0.0f, 1.0f, 50.0f, 50.0f);
+
+ var matrix = new Matrix(matrix3x2);
+ Assert.That(matrix.MatrixElements, Is.EqualTo(matrix3x2));
+ }
+
+ [Test]
+ public void Constructor_RectAndPoints()
+ {
+ var rect = new RectangleF(0, 0, 100, 100);
+ var plgpts = new PointF[] { new(0, 0), new(100, 0), new(0, 100) };
+
+ var matrix = new Matrix(rect, plgpts);
+ Assert.That(matrix.Elements, Is.EqualTo(new float[] { 1, 0, 0, 1, 0, 0 }).Within(TOLERANCE));
+ }
+
+ [Test]
+ public void Operator_Equality()
+ {
+ var matrix1 = new Matrix(1, 2, 3, 4, 5, 6);
+ var matrix2 = new Matrix(1, 2, 3, 4, 5, 6);
+ Assert.Multiple(() =>
+ {
+ Assert.That(matrix1 == matrix2, Is.True);
+ Assert.That(matrix1 != matrix2, Is.False);
+ });
+ }
+
+ [Test]
+ public void Method_Equals()
+ {
+ var matrix1 = new Matrix(1, 2, 3, 4, 5, 6);
+
+ var matrix2 = new Matrix(1, 2, 3, 4, 5, 6);
+ Assert.That(matrix1.Equals(matrix2), Is.True);
+
+ var matrix3 = new Matrix(6, 5, 4, 3, 2, 1);
+ Assert.That(matrix1.Equals(matrix3), Is.False);
+ }
+
+ [Test]
+ public void Method_Clone()
+ {
+ var original = new Matrix(1, 2, 3, 4, 5, 6);
+
+ var clone = original.Clone() as Matrix;
+ Assert.Multiple(() =>
+ {
+ Assert.That(clone, Is.Not.Null);
+ Assert.That(clone, Is.Not.SameAs(original));
+ Assert.That(clone, Is.EqualTo(original));
+ Assert.That(ReferenceEquals(clone, original), Is.False);
+ Assert.That(clone.Elements, Is.EqualTo(original.Elements));
+ });
+ }
+
+ [Test]
+ public void Method_Invert()
+ {
+ var matrix = new Matrix(1, 2, 3, 4, 5, 6);
+ Assert.Multiple(() =>
+ {
+ Assert.That(matrix.IsInvertible, Is.True);
+
+ matrix.Invert();
+ Assert.That(matrix.Elements, Is.EqualTo(new[] { -2, 1, 1.5f, -0.5f, 1, -2 }).Within(TOLERANCE));
+ });
+ }
+
+ [Test]
+ public void Method_Multiply()
+ {
+ var matrix1 = new Matrix(1, 0, 0, 1, 10, 10);
+ var matrix2 = new Matrix(1, 0, 0, 1, 20, 20);
+
+ matrix1.Multiply(matrix2);
+ Assert.That(matrix1.Elements, Is.EqualTo(new[] { 1, 0, 0, 1, 30, 30 }));
+ }
+
+ [Test]
+ public void Method_Reset()
+ {
+ var matrix = new Matrix(1, 2, 3, 4, 5, 6);
+
+ matrix.Reset();
+ Assert.That(matrix.IsIdentity, Is.True);
+ }
+
+ [Test]
+ public void Method_Rotate()
+ {
+ var matrix = new Matrix(1, 2, 3, 4, 5, 6);
+
+ matrix.Rotate(45);
+ Assert.That(matrix.Elements, Is.EqualTo(new float[] { 2.828427f, 4.24264f, 1.414214f, 1.414214f, 5, 6 }).Within(TOLERANCE));
+ }
+
+ [Test]
+ public void Method_RotateAt()
+ {
+ var matrix = new Matrix(1, 2, 3, 4, 5, 6);
+
+ matrix.RotateAt(45, new PointF(10, 10));
+ Assert.That(matrix.Elements, Is.EqualTo(new float[] { 2.828427f, 4.24264f, 1.414214f, 1.414214f, 2.573596f, 9.431463f }).Within(TOLERANCE));
+ }
+
+ [Test]
+ public void Method_Scale()
+ {
+ var matrix = new Matrix();
+
+ matrix.Scale(2, 2);
+ Assert.That(matrix.Elements, Is.Not.EqualTo(new[] { 2, 4, 9, 12, 5, 6 }).Within(TOLERANCE));
+ }
+
+ [Test]
+ public void Method_Shear()
+ {
+ var matrix = new Matrix(1, 2, 3, 4, 5, 6);
+
+ matrix.Shear(1, 2);
+ Assert.That(matrix.Elements, Is.EqualTo(new[] { 7, 10, 4, 6, 5, 6 }).Within(TOLERANCE));
+ }
+
+ [Test]
+ public void Method_Translate()
+ {
+ var matrix = new Matrix(1, 2, 3, 4, 5, 6);
+
+ matrix.Translate(50, 50);
+ Assert.Multiple(() =>
+ {
+ Assert.That(matrix.OffsetX, Is.EqualTo(205));
+ Assert.That(matrix.OffsetY, Is.EqualTo(306));
+ });
+ }
+
+
+ [Test]
+ public void Method_TransformPoints()
+ {
+ var matrix = new Matrix(1, 2, 3, 4, 5, 6);
+ var points = new PointF[] { new(0, 0), new(10, 10) };
+
+ matrix.TransformPoints(points);
+ Assert.Multiple(() =>
+ {
+ Assert.That(points[0], Is.EqualTo(new PointF(5, 6)));
+ Assert.That(points[1], Is.EqualTo(new PointF(45, 66)));
+ });
+ }
+
+ [Test]
+ public void Method_TransformVectors_ShouldTransformVectorsCorrectly()
+ {
+ var matrix = new Matrix(1, 2, 3, 4, 5, 6);
+ var vectors = new Point[] { new(1, 0), new(0, 1) };
+
+ matrix.TransformVectors(vectors);
+ Assert.Multiple(() =>
+ {
+ Assert.That(vectors[0], Is.EqualTo(new Point(1, 2)));
+ Assert.That(vectors[1], Is.EqualTo(new Point(3, 4)));
+ });
+ }
+}
\ No newline at end of file
diff --git a/test/Common/Drawing2D/PathGradientBrushUnitTest.cs b/test/Common/Drawing2D/PathGradientBrushUnitTest.cs
new file mode 100644
index 0000000..ada0b0d
--- /dev/null
+++ b/test/Common/Drawing2D/PathGradientBrushUnitTest.cs
@@ -0,0 +1,357 @@
+using System.IO;
+using GeneXus.Drawing.Drawing2D;
+
+namespace GeneXus.Drawing.Test.Drawing2D;
+
+internal class PathGradientBrushUnitTest
+{
+ [SetUp]
+ public void Setup()
+ {
+ }
+
+ [Test]
+ public void Constructor_Path()
+ {
+ var rect = new RectangleF(5, 5, 40, 40);
+ var color = Color.Red;
+
+ using var path = new GraphicsPath();
+ path.AddEllipse(rect);
+
+ using var brush = new PathGradientBrush(path) { CenterColor = color };
+ Assert.Multiple(() =>
+ {
+ var bounds = path.GetBounds();
+ var center = new PointF(rect.X + rect.Width / 2, rect.Y + rect.Height / 2);
+
+ Assert.That(brush.Rectangle, Is.EqualTo(bounds));
+ Assert.That(brush.WrapMode, Is.EqualTo(WrapMode.Clamp));
+ Assert.That(brush.Transform.IsIdentity, Is.True);
+ Assert.That(brush.FocusScales, Is.EqualTo(new PointF(0, 0)));
+ Assert.That(brush.SurroundColors, Is.EqualTo(new[] { Color.White }));
+
+ Assert.That(brush.CenterColor, Is.EqualTo(color));
+ Assert.That(brush.CenterPoint, Is.EqualTo(center));
+
+ Assert.That(brush.Blend.Factors, Is.EqualTo(new[] { 1f }));
+ Assert.That(brush.Blend.Positions, Is.EqualTo(new[] { 0f }));
+
+ Assert.That(brush.InterpolationColors.Colors, Is.EqualTo(new[] { Color.Empty }));
+ Assert.That(brush.InterpolationColors.Positions, Is.EqualTo(new[] { 0f }));
+
+ string filepath = Path.Combine("brush", "path", $"Circle.png");
+ float similarity = Utils.CompareImage(filepath, brush, true);
+ Assert.That(similarity, Is.GreaterThan(0.9));
+ });
+ }
+
+ [Test]
+ [TestCase(WrapMode.Tile)]
+ [TestCase(WrapMode.Clamp)]
+ public void Constructor_Points(WrapMode mode)
+ {
+ var points = new PointF[] { new(0, 0), new(50, 0), new(25, 25) };
+ var color = Color.Red;
+
+ using var brush = new PathGradientBrush(points, mode) { CenterColor = color };
+ Assert.Multiple(() =>
+ {
+ var bounds = Utils.GetBoundingRectangle(points);
+ var center = Utils.GetCenterPoint(points);
+
+ Assert.That(brush.Rectangle, Is.EqualTo(bounds));
+ Assert.That(brush.WrapMode, Is.EqualTo(mode));
+ Assert.That(brush.Transform.IsIdentity, Is.True);
+ Assert.That(brush.FocusScales, Is.EqualTo(PointF.Empty));
+ Assert.That(brush.SurroundColors, Is.EqualTo(new[] { Color.White }));
+
+ Assert.That(brush.CenterColor, Is.EqualTo(color));
+ Assert.That(brush.CenterPoint, Is.EqualTo(center));
+
+ Assert.That(brush.Blend.Factors, Is.EqualTo(new[] { 1f }));
+ Assert.That(brush.Blend.Positions, Is.EqualTo(new[] { 0f }));
+
+ Assert.That(brush.InterpolationColors.Colors, Is.EqualTo(new[] { Color.Empty }));
+ Assert.That(brush.InterpolationColors.Positions, Is.EqualTo(new[] { 0f }));
+
+ string filepath = Path.Combine("brush", "path", $"Triangle{mode}.png");
+ float similarity = Utils.CompareImage(filepath, brush, true);
+ Assert.That(similarity, Is.GreaterThan(0.9));
+ });
+ }
+
+ [Test]
+ public void Property_Blend()
+ {
+ var points = new PointF[] { new(0, 0), new(50, 0), new(25, 25) };
+ var color = Color.Red;
+ var blend = new Blend()
+ {
+ Factors = new float[] { 0.1f, 0.9f, 0.1f },
+ Positions = new float[] { 0.0f, 0.4f, 1.0f }
+ };
+
+ using var brush = new PathGradientBrush(points) { CenterColor = color, Blend = blend };
+ Assert.Multiple(() =>
+ {
+ var bounds = Utils.GetBoundingRectangle(points);
+ var center = Utils.GetCenterPoint(points);
+
+ Assert.That(brush.Rectangle, Is.EqualTo(bounds));
+ Assert.That(brush.WrapMode, Is.EqualTo(WrapMode.Clamp));
+ Assert.That(brush.Transform.IsIdentity, Is.True);
+ Assert.That(brush.FocusScales, Is.EqualTo(PointF.Empty));
+ Assert.That(brush.SurroundColors, Is.EqualTo(new[] { Color.White }));
+
+ Assert.That(brush.CenterColor, Is.EqualTo(color));
+ Assert.That(brush.CenterPoint, Is.EqualTo(center));
+
+ Assert.That(brush.Blend.Factors, Is.EqualTo(blend.Factors));
+ Assert.That(brush.Blend.Positions, Is.EqualTo(blend.Positions));
+
+ Assert.That(brush.InterpolationColors.Colors, Is.EqualTo(new[] { Color.Empty }));
+ Assert.That(brush.InterpolationColors.Positions, Is.EqualTo(new[] { 0f }));
+
+ string filepath = Path.Combine("brush", "path", $"TriangleBlended.png");
+ float similarity = Utils.CompareImage(filepath, brush, true);
+ Assert.That(similarity, Is.GreaterThan(0.9));
+ });
+ }
+
+ [Test]
+ public void Property_CenterPoint()
+ {
+ var rect = new RectangleF(5, 5, 40, 40);
+ var color = Color.Red;
+ var center = new PointF(15, 15);
+
+ using var path = new GraphicsPath();
+ path.AddEllipse(rect);
+
+ using var brush = new PathGradientBrush(path) { CenterColor = color, CenterPoint = center };
+ Assert.Multiple(() =>
+ {
+ var bounds = path.GetBounds();
+
+ Assert.That(brush.Rectangle, Is.EqualTo(bounds));
+ Assert.That(brush.WrapMode, Is.EqualTo(WrapMode.Clamp));
+ Assert.That(brush.Transform.IsIdentity, Is.True);
+ Assert.That(brush.FocusScales, Is.EqualTo(new PointF(0, 0)));
+ Assert.That(brush.SurroundColors, Is.EqualTo(new[] { Color.White }));
+
+ Assert.That(brush.CenterColor, Is.EqualTo(color));
+ Assert.That(brush.CenterPoint, Is.EqualTo(center));
+
+ Assert.That(brush.Blend.Factors, Is.EqualTo(new[] { 1f }));
+ Assert.That(brush.Blend.Positions, Is.EqualTo(new[] { 0f }));
+
+ Assert.That(brush.InterpolationColors.Colors, Is.EqualTo(new[] { Color.Empty }));
+ Assert.That(brush.InterpolationColors.Positions, Is.EqualTo(new[] { 0f }));
+
+ string filepath = Path.Combine("brush", "path", $"CircleRecentered.png");
+ float similarity = Utils.CompareImage(filepath, brush, true);
+ Assert.That(similarity, Is.GreaterThan(0.9));
+ });
+ }
+
+ [Test]
+ public void Property_FocusScales_Circular()
+ {
+ var rect = new RectangleF(5, 5, 40, 40);
+ var color = Color.Red;
+ var focus = new PointF(0.50f, 0.25f);
+
+ using var path = new GraphicsPath();
+ path.AddEllipse(rect);
+
+ using var brush = new PathGradientBrush(path) { CenterColor = color, FocusScales = focus };
+ Assert.Multiple(() =>
+ {
+ var bounds = path.GetBounds();
+ var center = new PointF(rect.X + rect.Width / 2, rect.Y + rect.Height / 2);
+
+ Assert.That(brush.Rectangle, Is.EqualTo(bounds));
+ Assert.That(brush.WrapMode, Is.EqualTo(WrapMode.Clamp));
+ Assert.That(brush.Transform.IsIdentity, Is.True);
+ Assert.That(brush.FocusScales, Is.EqualTo(focus));
+ Assert.That(brush.SurroundColors, Is.EqualTo(new[] { Color.White }));
+
+ Assert.That(brush.CenterColor, Is.EqualTo(color));
+ Assert.That(brush.CenterPoint, Is.EqualTo(center));
+
+ Assert.That(brush.Blend.Factors, Is.EqualTo(new[] { 1f }));
+ Assert.That(brush.Blend.Positions, Is.EqualTo(new[] { 0f }));
+
+ Assert.That(brush.InterpolationColors.Colors, Is.EqualTo(new[] { Color.Empty }));
+ Assert.That(brush.InterpolationColors.Positions, Is.EqualTo(new[] { 0f }));
+
+ string filepath = Path.Combine("brush", "path", $"CircleRefocused.png");
+ float similarity = Utils.CompareImage(filepath, brush, true);
+ Assert.That(similarity, Is.GreaterThan(0.9));
+ });
+ }
+
+ [Test]
+ public void Property_FocusScales_Triangular()
+ {
+ var points = new PointF[] { new(0, 0), new(50, 0), new(25, 25) };
+ var color = Color.Red;
+ var focus = new PointF(0.50f, 0.25f);
+
+ using var brush = new PathGradientBrush(points) { CenterColor = color, FocusScales = focus };
+ Assert.Multiple(() =>
+ {
+ var bounds = Utils.GetBoundingRectangle(points);
+ var center = Utils.GetCenterPoint(points);
+
+ Assert.That(brush.Rectangle, Is.EqualTo(bounds));
+ Assert.That(brush.WrapMode, Is.EqualTo(WrapMode.Clamp));
+ Assert.That(brush.Transform.IsIdentity, Is.True);
+ Assert.That(brush.FocusScales, Is.EqualTo(focus));
+ Assert.That(brush.SurroundColors, Is.EqualTo(new[] { Color.White }));
+
+ Assert.That(brush.CenterColor, Is.EqualTo(color));
+ Assert.That(brush.CenterPoint, Is.EqualTo(center));
+
+ Assert.That(brush.Blend.Factors, Is.EqualTo(new[] { 1f }));
+ Assert.That(brush.Blend.Positions, Is.EqualTo(new[] { 0f }));
+
+ Assert.That(brush.InterpolationColors.Colors, Is.EqualTo(new[] { Color.Empty }));
+ Assert.That(brush.InterpolationColors.Positions, Is.EqualTo(new[] { 0f }));
+
+ string filepath = Path.Combine("brush", "path", $"TriangleRefocused.png");
+ float similarity = Utils.CompareImage(filepath, brush, true);
+ Assert.That(similarity, Is.GreaterThan(0.9));
+ });
+ }
+
+ [Test]
+ public void Property_InterpolationColors()
+ {
+ var points = new PointF[] { new(0, 0), new(50, 0), new(25, 25) };
+ var blend = new ColorBlend()
+ {
+ Colors = new Color[] { Color.DarkGreen, Color.Aqua, Color.Blue },
+ Positions = new float[] { 0.0f, 0.4f, 1.0f }
+ };
+
+ using var brush = new PathGradientBrush(points) { InterpolationColors = blend };
+ Assert.Multiple(() =>
+ {
+ var bounds = Utils.GetBoundingRectangle(points);
+ var center = Utils.GetCenterPoint(points);
+
+ Assert.That(brush.Rectangle, Is.EqualTo(bounds));
+ Assert.That(brush.WrapMode, Is.EqualTo(WrapMode.Clamp));
+ Assert.That(brush.Transform.IsIdentity, Is.True);
+ Assert.That(brush.FocusScales, Is.EqualTo(PointF.Empty));
+ Assert.That(brush.SurroundColors, Is.EqualTo(new[] { Color.White }));
+
+ Assert.That(brush.CenterColor, Is.EqualTo(Color.White));
+ Assert.That(brush.CenterPoint, Is.EqualTo(center));
+
+ Assert.That(brush.Blend.Factors, Is.EqualTo(new[] { 1f }));
+ Assert.That(brush.Blend.Positions, Is.EqualTo(new[] { 0f }));
+
+ Assert.That(brush.InterpolationColors.Colors, Is.EqualTo(blend.Colors));
+ Assert.That(brush.InterpolationColors.Positions, Is.EqualTo(blend.Positions));
+
+ string filepath = Path.Combine("brush", "path", $"TriangleInterpolated.png");
+ float similarity = Utils.CompareImage(filepath, brush, true);
+ Assert.That(similarity, Is.GreaterThan(0.9));
+ });
+ }
+
+ [Test]
+ public void Property_SurroundedColors()
+ {
+ var points = new PointF[] { new(0, 0), new(50, 0), new(25, 25) };
+ var surrounded = new Color[] { Color.Green, Color.Yellow, Color.Blue };
+
+ using var brush = new PathGradientBrush(points) { SurroundColors = surrounded };
+ Assert.Multiple(() =>
+ {
+ var bounds = Utils.GetBoundingRectangle(points);
+ var center = Utils.GetCenterPoint(points);
+
+ Assert.That(brush.Rectangle, Is.EqualTo(bounds));
+ Assert.That(brush.WrapMode, Is.EqualTo(WrapMode.Clamp));
+ Assert.That(brush.Transform.IsIdentity, Is.True);
+ Assert.That(brush.FocusScales, Is.EqualTo(PointF.Empty));
+ Assert.That(brush.SurroundColors, Is.EqualTo(surrounded));
+
+ Assert.That(brush.CenterColor, Is.EqualTo(Color.Black));
+ Assert.That(brush.CenterPoint, Is.EqualTo(center));
+
+ Assert.That(brush.Blend.Factors, Is.EqualTo(new[] { 1f }));
+ Assert.That(brush.Blend.Positions, Is.EqualTo(new[] { 0f }));
+
+ Assert.That(brush.InterpolationColors.Colors, Is.EqualTo(new[] { Color.Empty }));
+ Assert.That(brush.InterpolationColors.Positions, Is.EqualTo(new[] { 0f }));
+
+ string filepath = Path.Combine("brush", "path", $"TriangleSurrounded.png");
+ float similarity = Utils.CompareImage(filepath, brush, true);
+ Assert.That(similarity, Is.GreaterThan(0.9));
+ });
+ }
+
+ [Test]
+ public void Property_Transform()
+ {
+ var rect = new RectangleF(5, 5, 40, 40);
+ var color = Color.Red;
+
+ using var path = new GraphicsPath();
+ path.AddEllipse(rect);
+
+ var transform = new Matrix();
+ transform.Scale(0.75f, 1f);
+
+ using var brush = new PathGradientBrush(path) { CenterColor = color, Transform = transform };
+ Assert.Multiple(() =>
+ {
+ var bounds = path.GetBounds();
+ var center = new PointF(rect.X + rect.Width / 2, rect.Y + rect.Height / 2);
+
+ Assert.That(brush.Rectangle, Is.EqualTo(bounds));
+ Assert.That(brush.WrapMode, Is.EqualTo(WrapMode.Clamp));
+ Assert.That(brush.Transform, Is.EqualTo(transform));
+ Assert.That(brush.FocusScales, Is.EqualTo(new PointF(0, 0)));
+ Assert.That(brush.SurroundColors, Is.EqualTo(new[] { Color.White }));
+
+ Assert.That(brush.CenterColor, Is.EqualTo(color));
+ Assert.That(brush.CenterPoint, Is.EqualTo(center));
+
+ Assert.That(brush.Blend.Factors, Is.EqualTo(new[] { 1f }));
+ Assert.That(brush.Blend.Positions, Is.EqualTo(new[] { 0f }));
+
+ Assert.That(brush.InterpolationColors.Colors, Is.EqualTo(new[] { Color.Empty }));
+ Assert.That(brush.InterpolationColors.Positions, Is.EqualTo(new[] { 0f }));
+
+ string filepath = Path.Combine("brush", "path", $"CircleTransform.png");
+ float similarity = Utils.CompareImage(filepath, brush, true);
+ Assert.That(similarity, Is.GreaterThan(0.9));
+ });
+ }
+
+ [Test]
+ public void Method_Clone()
+ {
+ var rect = new RectangleF(15, 15, 20, 20);
+
+ using var path = new GraphicsPath();
+ path.AddEllipse(rect);
+
+ using var brush1 = new PathGradientBrush(path) { WrapMode = WrapMode.Tile };
+ using var brush2 = brush1.Clone() as PathGradientBrush;
+ Assert.Multiple(() =>
+ {
+ Assert.That(brush2, Is.Not.Null);
+ Assert.That(brush2, Is.Not.SameAs(brush1));
+ Assert.That(brush2.Transform.Elements, Is.EqualTo(brush1.Transform.Elements));
+ Assert.That(brush2.Rectangle, Is.EqualTo(brush1.Rectangle));
+ Assert.That(brush2.WrapMode, Is.EqualTo(brush1.WrapMode));
+ });
+ }
+}
\ No newline at end of file
diff --git a/test/Common/FontFamilyUnitTest.cs b/test/Common/FontFamilyUnitTest.cs
index 30f59b9..69ab180 100644
--- a/test/Common/FontFamilyUnitTest.cs
+++ b/test/Common/FontFamilyUnitTest.cs
@@ -1,6 +1,5 @@
using GeneXus.Drawing.Text;
using System;
-using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
diff --git a/test/Common/GraphicsUnitTest.cs b/test/Common/GraphicsUnitTest.cs
new file mode 100644
index 0000000..6b6cf77
--- /dev/null
+++ b/test/Common/GraphicsUnitTest.cs
@@ -0,0 +1,928 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using GeneXus.Drawing.Drawing2D;
+using GeneXus.Drawing.Text;
+
+namespace GeneXus.Drawing.Test;
+
+internal class GraphicsUnitTest
+{
+ private static readonly string IMAGE_PATH = Path.Combine(
+ Directory.GetParent(Environment.CurrentDirectory).Parent.FullName,
+ "res", "images");
+
+ private static readonly string FONT_PATH = Path.Combine(
+ Directory.GetParent(Environment.CurrentDirectory).Parent.FullName,
+ "res", "fonts");
+
+ [SetUp]
+ public void Setup()
+ {
+ }
+
+
+ #region Factory methods
+
+ [Test]
+ public void Method_FromImage()
+ {
+ using var bitmap = new Bitmap(50, 50);
+ using var g = Graphics.FromImage(bitmap);
+ Assert.That(bitmap.GetPixel(25, 25), Is.EqualTo(Color.Empty));
+ }
+
+ #endregion
+
+
+ #region Propeties
+
+ [Test]
+ [TestCase(CompositingMode.SourceCopy, "#FF000080")]
+ [TestCase(CompositingMode.SourceOver, "#80007FFF")]
+ public void Property_CompositingMode(CompositingMode mode, string hex)
+ {
+ var color = Color.FromArgb(128, 255, 0, 0);
+ using var brush = new SolidBrush(color);
+
+ using var bitmap = new Bitmap(50, 50);
+ using var g = Graphics.FromImage(bitmap);
+
+ g.Clear(Color.Blue);
+ g.CompositingMode = mode;
+ g.FillRectangle(brush, 10, 10, 30, 30);
+
+ Assert.Multiple(() =>
+ {
+ var expected = Color.FromHex(hex);
+ Assert.That(bitmap.GetPixel(0, 0), Is.EqualTo(Color.Blue));
+ Assert.That(bitmap.GetPixel(25, 25), Is.EqualTo(expected));
+ });
+ }
+
+ [Test]
+ [TestCase(0, 0)]
+ [TestCase(25, 25)]
+ public void Property_RenderingOrigin(int x, int y)
+ {
+ using var brush = new HatchBrush(HatchStyle.DiagonalCross, Color.Green, Color.Orange);
+
+ using var bitmap = new Bitmap(50, 50);
+ using var g = Graphics.FromImage(bitmap);
+
+ g.Clear(Color.Blue);
+ g.RenderingOrigin = new Point(x, y);
+ g.FillRectangle(brush, 10, 10, 30, 30);
+
+ Assert.Multiple(() =>
+ {
+ string filepath = Path.Combine("graphics", $"RenderingOrigin_{x:D2}-{y:D2}.png");
+ float similarity = Utils.CompareImage(filepath, bitmap, true);
+ Assert.That(similarity, Is.GreaterThan(0.9));
+ });
+ }
+
+ #endregion
+
+
+ #region Utilities methods
+
+ [Test]
+ public void Method_Clear()
+ {
+ var color = Color.Red;
+
+ using var bitmap = new Bitmap(50, 50);
+ using var g = Graphics.FromImage(bitmap);
+
+ g.Clear(color);
+ Assert.Multiple(() =>
+ {
+ for (int i = 0; i < bitmap.Width; i++)
+ for (int j = 0; j < bitmap.Height; j++)
+ Assert.That(bitmap.GetPixel(i, j), Is.EqualTo(color));
+ });
+ }
+
+ [Test]
+ public void Method_CopyFromScreen()
+ {
+ using var bitmap = new Bitmap(50, 50);
+ using var g = Graphics.FromImage(bitmap);
+
+ var size = new Size(25, 25);
+ Assert.Throws(() =>
+ {
+ g.CopyFromScreen(0, 0, bitmap.Width, bitmap.Height, size);
+ });
+ }
+
+ [Test]
+ public void Method_GetNearestColor()
+ {
+ var color = Color.FromArgb(123, 200, 123);
+
+ using var bitmap = new Bitmap(50, 50);
+ using var g = Graphics.FromImage(bitmap);
+
+ var nearest = g.GetNearestColor(color);
+ Assert.Multiple(() =>
+ {
+ Assert.That(nearest.R, Is.EqualTo(color.R).Within(5));
+ Assert.That(nearest.G, Is.EqualTo(color.G).Within(5));
+ Assert.That(nearest.B, Is.EqualTo(color.B).Within(5));
+ });
+ }
+
+ [Test]
+ public void Method_IsVisible()
+ {
+ var color = Color.Red;
+ using var brush = new SolidBrush(color);
+
+ using var bitmap = new Bitmap(50, 50);
+ using var g = Graphics.FromImage(bitmap);
+
+ g.FillRectangle(brush, 10, 10, 30, 30);
+ Assert.Multiple(() =>
+ {
+ Assert.That(g.IsVisible(25, 25), Is.True);
+ Assert.That(g.IsVisible(50, 50), Is.False);
+ });
+ }
+
+ #endregion
+
+
+ #region State methods
+
+ [Test]
+ public void Method_SaveAndRestore()
+ {
+ // NOTE: Save/Restore only works over transform matrix in both System.Drawing and SkiaSharp
+ using var bitmap = new Bitmap(50, 50);
+ using var g = Graphics.FromImage(bitmap);
+
+ g.TranslateTransform(10, 10);
+
+ var state = g.Save();
+ Assert.Multiple(() =>
+ {
+ Assert.That(g.Transform.OffsetX, Is.EqualTo(10));
+ Assert.That(g.Transform.OffsetY, Is.EqualTo(10));
+ });
+
+ g.TranslateTransform(5, 5);
+ Assert.Multiple(() =>
+ {
+ Assert.That(g.Transform.OffsetX, Is.EqualTo(15));
+ Assert.That(g.Transform.OffsetY, Is.EqualTo(15));
+ });
+
+ g.Restore(state);
+ Assert.Multiple(() =>
+ {
+ Assert.That(g.Transform.OffsetX, Is.EqualTo(10));
+ Assert.That(g.Transform.OffsetY, Is.EqualTo(10));
+ });
+ }
+
+
+ [Test]
+ public void Method_BeginAndEndContainer()
+ {
+ using var bitmap = new Bitmap(50, 50);
+ using var g = Graphics.FromImage(bitmap);
+
+ g.TranslateTransform(10, 10);
+
+ var state = g.BeginContainer();
+ Assert.Multiple(() =>
+ {
+ Assert.That(g.Transform.OffsetX, Is.EqualTo(0));
+ Assert.That(g.Transform.OffsetY, Is.EqualTo(0));
+ });
+
+ g.TranslateTransform(5, 5);
+ Assert.Multiple(() =>
+ {
+ Assert.That(g.Transform.OffsetX, Is.EqualTo(5));
+ Assert.That(g.Transform.OffsetY, Is.EqualTo(5));
+ });
+
+ g.EndContainer(state);
+ Assert.Multiple(() =>
+ {
+ Assert.That(g.Transform.OffsetX, Is.EqualTo(10));
+ Assert.That(g.Transform.OffsetY, Is.EqualTo(10));
+ });
+ }
+
+ #endregion
+
+
+ #region Draw methods
+
+ [Test]
+ public void Method_DrawArc()
+ {
+ var color = Color.Red;
+ using var pen = new Pen(color);
+
+ using var bitmap = new Bitmap(50, 50);
+ using var g = Graphics.FromImage(bitmap);
+
+ g.DrawArc(pen, 0, 0, 50, 50, 0, 45);
+ Assert.Multiple(() =>
+ {
+ string filepath = Path.Combine("graphics", $"DrawArc.png");
+ float similarity = Utils.CompareImage(filepath, bitmap, true);
+ Assert.That(similarity, Is.GreaterThan(0.9));
+ });
+ }
+
+ [Test]
+ public void Method_DrawBezier()
+ {
+ var color = Color.Red;
+ using var pen = new Pen(color);
+
+ using var bitmap = new Bitmap(50, 50);
+ using var g = Graphics.FromImage(bitmap);
+
+ g.DrawBezier(pen, 0, 25, 5, 15, 15, 5, 25, 0);
+ Assert.Multiple(() =>
+ {
+ string filepath = Path.Combine("graphics", $"DrawBezier.png");
+ float similarity = Utils.CompareImage(filepath, bitmap, true);
+ Assert.That(similarity, Is.GreaterThan(0.9));
+ });
+ }
+
+ [Test]
+ public void Method_DrawClosedCurve()
+ {
+ var color = Color.Red;
+ using var pen = new Pen(color);
+
+ using var bitmap = new Bitmap(50, 50);
+ using var g = Graphics.FromImage(bitmap);
+
+ var points = new Point[] { new(0, 25), new(25, 0), new(25, 25) };
+ var tension = 0.75f;
+ var mode = FillMode.Winding;
+
+ g.DrawClosedCurve(pen, points, tension, mode);
+ Assert.Multiple(() =>
+ {
+ string filepath = Path.Combine("graphics", $"DrawClosedCurve.png");
+ float similarity = Utils.CompareImage(filepath, bitmap, true);
+ Assert.That(similarity, Is.GreaterThan(0.9));
+ });
+ }
+
+ [Test]
+ public void Method_DrawCurve()
+ {
+ var color = Color.Red;
+ using var pen = new Pen(color);
+
+ using var bitmap = new Bitmap(50, 50);
+ using var g = Graphics.FromImage(bitmap);
+
+ var points = new Point[] { new(0, 25), new(25, 0), new(25, 25) };
+ var tension = 0.75f;
+
+ g.DrawCurve(pen, points, tension);
+ Assert.Multiple(() =>
+ {
+ string filepath = Path.Combine("graphics", $"DrawCurve.png");
+ float similarity = Utils.CompareImage(filepath, bitmap, true);
+ Assert.That(similarity, Is.GreaterThan(0.9));
+ });
+ }
+
+ [Test]
+ public void Method_DrawEllipse()
+ {
+ var color = Color.Red;
+ using var pen = new Pen(color);
+
+ using var bitmap = new Bitmap(50, 50);
+ using var g = Graphics.FromImage(bitmap);
+
+ g.DrawEllipse(pen, 10, 10, 30, 30);
+ Assert.Multiple(() =>
+ {
+ string filepath = Path.Combine("graphics", $"DrawEllipse.png");
+ float similarity = Utils.CompareImage(filepath, bitmap, true);
+ Assert.That(similarity, Is.GreaterThan(0.9));
+ });
+ }
+
+ [Test]
+ public void Method_DrawIcon()
+ {
+ // TODO: test DrawIcon method
+ }
+
+ [Test]
+ public void Method_DrawImage()
+ {
+ var filePath = Path.Combine(IMAGE_PATH, "Sample.png");
+ using var image = Image.FromFile(filePath);
+
+ var portion = new Rectangle(10, 10, 30, 30);
+
+ using var bitmap = new Bitmap(50, 50);
+ using var g = Graphics.FromImage(bitmap);
+
+ g.DrawImage(image, 10, 10, portion, GraphicsUnit.Pixel);
+ Assert.Multiple(() =>
+ {
+ string filepath = Path.Combine("graphics", $"DrawImage.png");
+ float similarity = Utils.CompareImage(filepath, bitmap, true);
+ Assert.That(similarity, Is.GreaterThan(0.9));
+ });
+ }
+
+ [Test]
+ public void Method_DrawLine()
+ {
+ var color = Color.Red;
+ using var pen = new Pen(color);
+
+ using var bitmap = new Bitmap(50, 50);
+ using var g = Graphics.FromImage(bitmap);
+
+ g.DrawLine(pen, 10, 10, 40, 40);
+ Assert.Multiple(() =>
+ {
+ string filepath = Path.Combine("graphics", $"DrawLine.png");
+ float similarity = Utils.CompareImage(filepath, bitmap, true);
+ Assert.That(similarity, Is.GreaterThan(0.9));
+ });
+ }
+
+ [Test]
+ public void Method_DrawPath()
+ {
+ var color = Color.Red;
+ using var pen = new Pen(color);
+
+ using var bitmap = new Bitmap(50, 50);
+ using var g = Graphics.FromImage(bitmap);
+
+ using var path = new GraphicsPath();
+ path.AddLine(0, 0, 50, 0);
+ path.AddLine(50, 0, 25, 25);
+ path.AddLine(25, 25, 0, 0);
+ path.CloseFigure(); // defines a triangle
+
+ g.DrawPath(pen, path);
+ Assert.Multiple(() =>
+ {
+ string filepath = Path.Combine("graphics", $"DrawPath.png");
+ float similarity = Utils.CompareImage(filepath, bitmap, true);
+ Assert.That(similarity, Is.GreaterThan(0.9));
+ });
+ }
+
+ [Test]
+ public void Method_DrawPie()
+ {
+ var color = Color.Red;
+ using var pen = new Pen(color);
+
+ using var bitmap = new Bitmap(50, 50);
+ using var g = Graphics.FromImage(bitmap);
+
+ g.DrawPie(pen, 10, 10, 30, 30, 0, 45);
+ Assert.Multiple(() =>
+ {
+ string filepath = Path.Combine("graphics", $"DrawPie.png");
+ float similarity = Utils.CompareImage(filepath, bitmap, true);
+ Assert.That(similarity, Is.GreaterThan(0.9));
+ });
+ }
+
+ [Test]
+ public void Method_DrawPolygon()
+ {
+ var color = Color.Red;
+ using var pen = new Pen(color);
+
+ using var bitmap = new Bitmap(50, 50);
+ using var g = Graphics.FromImage(bitmap);
+
+ var points = new[] { new PointF(0, 0), new PointF(25, 25), new PointF(50, 0), new PointF(25, 50) };
+
+ g.DrawPolygon(pen, points);
+ Assert.Multiple(() =>
+ {
+ string filepath = Path.Combine("graphics", $"DrawPolygon.png");
+ float similarity = Utils.CompareImage(filepath, bitmap, true);
+ Assert.That(similarity, Is.GreaterThan(0.9));
+ });
+ }
+
+ [Test]
+ public void Method_DrawRectangle()
+ {
+ var color = Color.Red;
+ using var pen = new Pen(color);
+
+ using var bitmap = new Bitmap(50, 50);
+ using var g = Graphics.FromImage(bitmap);
+
+ g.DrawRectangle(pen, 10, 10, 30, 30);
+ Assert.Multiple(() =>
+ {
+ string filepath = Path.Combine("graphics", $"DrawRectangle.png");
+ float similarity = Utils.CompareImage(filepath, bitmap, true);
+ Assert.That(similarity, Is.GreaterThan(0.9));
+ });
+ }
+
+ [Test]
+ public void Method_DrawString()
+ {
+ string fontPath = Path.Combine(FONT_PATH, "Montserrat-Regular.ttf");
+
+ using var pfc = new PrivateFontCollection();
+ pfc.AddFontFile(fontPath);
+
+ var font = new Font(pfc.Families[0], 10);
+
+ var color = Color.Red;
+ using var brush = new SolidBrush(color);
+
+ using var bitmap = new Bitmap(50, 50);
+ using var g = Graphics.FromImage(bitmap);
+
+ var bounds = new RectangleF(0, 0, 40, 40);
+
+ var flags = StringFormatFlags.NoWrap;
+ var format = new StringFormat(flags)
+ {
+ HotkeyPrefix = HotkeyPrefix.Show,
+ Trimming = StringTrimming.EllipsisCharacter,
+ };
+
+ g.TextRenderingHint = TextRenderingHint.SingleBitPerPixel;
+ g.DrawString("&hello\nworld\n123", font, brush, bounds, format);
+ Assert.Multiple(() =>
+ {
+ string filepath = Path.Combine("graphics", $"DrawString.png");
+ float similarity = Utils.CompareImage(filepath, bitmap, true);
+ Assert.That(similarity, Is.GreaterThan(0.85));
+ });
+ }
+
+ #endregion
+
+
+ #region Fill methods
+
+ [Test]
+ public void Method_FillClosedCurve()
+ {
+ // TODO: test FillClosedCurve method
+ var color = Color.Red;
+ using var brush = new SolidBrush(color);
+
+ using var bitmap = new Bitmap(50, 50);
+ using var g = Graphics.FromImage(bitmap);
+
+ var points = new Point[] { new(0, 25), new(25, 0), new(25, 25) };
+ var tension = 0.75f;
+ var mode = FillMode.Winding;
+
+ g.FillClosedCurve(brush, points, mode, tension);
+ Assert.Multiple(() =>
+ {
+ string filepath = Path.Combine("graphics", $"FillClosedCurve.png");
+ float similarity = Utils.CompareImage(filepath, bitmap, true);
+ Assert.That(similarity, Is.GreaterThan(0.9).Within(0.1));
+ });
+ }
+
+ [Test]
+ public void Method_FillEllipse()
+ {
+ var color = Color.Red;
+ using var brush = new SolidBrush(color);
+
+ using var bitmap = new Bitmap(50, 50);
+ using var g = Graphics.FromImage(bitmap);
+
+ g.FillEllipse(brush, 10, 10, 30, 30);
+ Assert.Multiple(() =>
+ {
+ string filepath = Path.Combine("graphics", $"FillEllipse.png");
+ float similarity = Utils.CompareImage(filepath, bitmap, true);
+ Assert.That(similarity, Is.GreaterThan(0.9));
+ });
+ }
+
+ [Test]
+ public void Method_FillPath()
+ {
+ var color = Color.Red;
+ using var brush = new SolidBrush(color);
+
+ using var bitmap = new Bitmap(50, 50);
+ using var g = Graphics.FromImage(bitmap);
+
+ using var path = new GraphicsPath();
+ path.AddLine(0, 0, 50, 0);
+ path.AddLine(50, 0, 25, 25);
+ path.AddLine(25, 25, 0, 0);
+ path.CloseFigure(); // defines a triangle
+
+ g.FillPath(brush, path);
+ Assert.Multiple(() =>
+ {
+ string filepath = Path.Combine("graphics", $"FillPath.png");
+ float similarity = Utils.CompareImage(filepath, bitmap, true);
+ Assert.That(similarity, Is.GreaterThan(0.9));
+ });
+ }
+
+ [Test]
+ public void Method_FillPie()
+ {
+ var color = Color.Red;
+ using var brush = new SolidBrush(color);
+
+ using var bitmap = new Bitmap(50, 50);
+ using var g = Graphics.FromImage(bitmap);
+
+ g.FillPie(brush, 10, 10, 30, 30, 0, 45);
+ Assert.Multiple(() =>
+ {
+ string filepath = Path.Combine("graphics", $"FillPie.png");
+ float similarity = Utils.CompareImage(filepath, bitmap, true);
+ Assert.That(similarity, Is.GreaterThan(0.9));
+ });
+ }
+
+ [Test]
+ public void Method_FillPolygon()
+ {
+ var color = Color.Red;
+ using var brush = new SolidBrush(color);
+
+ using var bitmap = new Bitmap(50, 50);
+ using var g = Graphics.FromImage(bitmap);
+
+ var points = new[] { new PointF(0, 0), new PointF(25, 25), new PointF(50, 0), new PointF(25, 50) };
+
+ g.FillPolygon(brush, points);
+ Assert.Multiple(() =>
+ {
+ string filepath = Path.Combine("graphics", $"FillPolygon.png");
+ float similarity = Utils.CompareImage(filepath, bitmap, true);
+ Assert.That(similarity, Is.GreaterThan(0.9));
+ });
+ }
+
+ [Test]
+ public void Method_FillRectangle()
+ {
+ var color = Color.Red;
+ using var brush = new SolidBrush(color);
+
+ using var bitmap = new Bitmap(50, 50);
+ using var g = Graphics.FromImage(bitmap);
+
+ g.FillRectangle(brush, 10, 10, 30, 30);
+ Assert.Multiple(() =>
+ {
+ string filepath = Path.Combine("graphics", $"FillRectangle.png");
+ float similarity = Utils.CompareImage(filepath, bitmap, true);
+ Assert.That(similarity, Is.GreaterThan(0.9));
+ });
+ }
+
+ [Test]
+ public void Method_FillRegion()
+ {
+ var color = Color.Red;
+ using var brush = new SolidBrush(color);
+
+ using var bitmap = new Bitmap(50, 50);
+ using var g = Graphics.FromImage(bitmap);
+
+ var path = new GraphicsPath();
+ path.AddLine(10, 10, 40, 10);
+ path.AddBezier(40, 10, 30, 40, 20, 40, 10, 10);
+ path.CloseFigure();
+
+ using var region = new Region(path);
+
+ g.FillRegion(brush, region);
+ Assert.Multiple(() =>
+ {
+ string filepath = Path.Combine("graphics", $"FillRegion.png");
+ float similarity = Utils.CompareImage(filepath, bitmap, true);
+ Assert.That(similarity, Is.GreaterThan(0.9));
+ });
+ }
+
+ #endregion
+
+
+ #region Text methods
+
+ [Test]
+ public void Method_MeasureCharacterRanges()
+ {
+ string fontPath = Path.Combine(FONT_PATH, "Montserrat-Regular.ttf");
+
+ using var pfc = new PrivateFontCollection();
+ pfc.AddFontFile(fontPath);
+
+ var font = new Font(pfc.Families[0], 10);
+
+ var flags = StringFormatFlags.NoWrap;
+ var format = new StringFormat(flags)
+ {
+ HotkeyPrefix = HotkeyPrefix.Show,
+ Trimming = StringTrimming.EllipsisCharacter,
+ };
+
+ var charRanges = new[] { new CharacterRange(0, 4) };
+ format.SetMeasurableCharacterRanges(charRanges);
+
+ using var bitmap = new Bitmap(50, 50);
+ using var g = Graphics.FromImage(bitmap);
+
+ var bounds = new RectangleF(0, 0, bitmap.Width, bitmap.Height);
+
+ var regions = g.MeasureCharacterRanges("Hello World", font, bounds, format);
+ Assert.Multiple(() =>
+ {
+ Assert.That(regions, Has.Length.EqualTo(charRanges.Length));
+ var bounds = regions[0].GetBounds(g);
+ Assert.That(bounds.X, Is.EqualTo(3).Within(1));
+ Assert.That(bounds.Y, Is.EqualTo(0).Within(1));
+ Assert.That(bounds.Width, Is.EqualTo(27).Within(5)); // NOTE: huge difference but skia draws text's path in pixel, that's why this value is smaller than expected
+ Assert.That(bounds.Height, Is.EqualTo(17).Within(2));
+ });
+ }
+
+ [Test]
+ public void Method_MeasureString()
+ {
+ string fontPath = Path.Combine(FONT_PATH, "Montserrat-Regular.ttf");
+
+ using var pfc = new PrivateFontCollection();
+ pfc.AddFontFile(fontPath);
+
+ var font = new Font(pfc.Families[0], 10);
+
+ using var bitmap = new Bitmap(50, 50);
+ using var g = Graphics.FromImage(bitmap);
+
+ var measure = g.MeasureString("Hello World", font);
+ Assert.Multiple(() =>
+ {
+ Assert.That(measure.Width, Is.EqualTo(85).Within(11)); // NOTE: huge difference but skia draws text's path in pixel, that's why this value is smaller than expected
+ Assert.That(measure.Height, Is.EqualTo(17).Within(2));
+ });
+ }
+
+ #endregion
+
+
+ #region Clip methods
+
+ [Test]
+ public void Method_ExcludeClip()
+ {
+ var color = Color.Red;
+ var clip1 = new Rectangle(0, 0, 25, 25);
+ var clip2 = new Rectangle(10, 10, 30, 30);
+
+ using var bitmap = new Bitmap(50, 50);
+ using var g = Graphics.FromImage(bitmap);
+
+ g.SetClip(clip1);
+ g.ExcludeClip(clip2);
+ g.Clear(color);
+ Assert.Multiple(() =>
+ {
+ string filepath = Path.Combine("graphics", $"ClipExclude.png");
+ float similarity = Utils.CompareImage(filepath, bitmap, true);
+ Assert.That(similarity, Is.GreaterThan(0.9));
+ });
+ }
+
+ [Test]
+ public void Method_IntersectClip()
+ {
+ var color = Color.Red;
+ var clip1 = new Rectangle(0, 0, 25, 25);
+ var clip2 = new Rectangle(10, 10, 10, 10);
+
+ using var bitmap = new Bitmap(50, 50);
+ using var g = Graphics.FromImage(bitmap);
+
+ g.SetClip(clip1);
+ g.IntersectClip(clip2);
+ g.Clear(color);
+ Assert.Multiple(() =>
+ {
+ string filepath = Path.Combine("graphics", $"ClipIntersect.png");
+ float similarity = Utils.CompareImage(filepath, bitmap, true);
+ Assert.That(similarity, Is.GreaterThan(0.9));
+ });
+ }
+
+ [Test]
+ public void Method_TranslateClip()
+ {
+ var color = Color.Red;
+ var clip = new Rectangle(0, 0, 25, 25);
+
+ using var bitmap = new Bitmap(50, 50);
+ using var g = Graphics.FromImage(bitmap);
+
+ g.SetClip(clip);
+ g.TranslateClip(10, 10);
+ g.Clear(color);
+ Assert.Multiple(() =>
+ {
+ string filepath = Path.Combine("graphics", $"ClipTranslate.png");
+ float similarity = Utils.CompareImage(filepath, bitmap, true);
+ Assert.That(similarity, Is.GreaterThan(0.9));
+ });
+ }
+
+ [Test]
+ public void Method_SetClip()
+ {
+ var color = Color.Red;
+ var clip = new Rectangle(0, 0, 25, 25);
+
+ using var bitmap = new Bitmap(50, 50);
+ using var g = Graphics.FromImage(bitmap);
+
+ g.SetClip(clip);
+ g.Clear(color);
+ Assert.Multiple(() =>
+ {
+ for (int i = 0; i < bitmap.Width; i++)
+ for (int j = 0; j < bitmap.Height; j++)
+ Assert.That(bitmap.GetPixel(i, j), Is.EqualTo(clip.Contains(i, j) ? color : Color.Empty));
+ });
+ }
+
+ [Test]
+ public void Method_ResetClip()
+ {
+ var color = Color.Red;
+ var clip = new Rectangle(0, 0, 25, 25);
+
+ using var bitmap = new Bitmap(50, 50);
+ using var g = Graphics.FromImage(bitmap);
+
+ g.SetClip(clip);
+ g.Clear(color);
+ g.ResetClip();
+ Assert.Multiple(() =>
+ {
+ for (int i = 0; i < bitmap.Width; i++)
+ for (int j = 0; j < bitmap.Height; j++)
+ Assert.That(bitmap.GetPixel(i, j), Is.EqualTo(color));
+ });
+ }
+
+ #endregion
+
+
+ #region Transform methods
+
+ [Test]
+ public void Method_MultiplyTransform()
+ {
+ using var bitmap = new Bitmap(50, 50);
+ using var g = Graphics.FromImage(bitmap);
+
+ using var matrix = new Matrix(6, 5, 4, 3, 2, 1);
+
+ g.Transform = new Matrix(1, 2, 3, 4, 5, 6);
+ g.MultiplyTransform(matrix);
+ Assert.That(g.Transform.Elements, Is.EqualTo(new[] { 21, 32, 13, 20, 10, 14 }).Within(0.001f));
+ }
+
+ [Test]
+ public void Method_ResetTransform()
+ {
+ using var bitmap = new Bitmap(50, 50);
+ using var g = Graphics.FromImage(bitmap);
+
+ g.TranslateTransform(20, 30);
+ g.ResetTransform();
+ Assert.That(g.Transform.IsIdentity, Is.True);
+ }
+
+ [Test]
+ public void Method_RotateTransform()
+ {
+ using var bitmap = new Bitmap(50, 50);
+ using var g = Graphics.FromImage(bitmap);
+
+ g.RotateTransform(45);
+ Assert.That(g.Transform.Elements, Is.EqualTo(new[] { 0.707f, 0.707f, -0.707f, 0.707f, 0, 0 }).Within(0.001f));
+ }
+
+ [Test]
+ public void Method_ScaleTransform()
+ {
+ using var bitmap = new Bitmap(50, 50);
+ using var g = Graphics.FromImage(bitmap);
+
+ g.ScaleTransform(0.50f, 0.25f);
+ Assert.That(g.Transform.Elements, Is.EqualTo(new[] { 0.5f, 0, 0, 0.25f, 0, 0 }).Within(0.001f));
+ }
+
+ [Test]
+ public void Method_TranslateTransform()
+ {
+ using var bitmap = new Bitmap(50, 50);
+ using var g = Graphics.FromImage(bitmap);
+
+ g.TranslateTransform(50, 50f);
+ Assert.That(g.Transform.Elements, Is.EqualTo(new[] { 1, 0, 0, 1, 50, 50 }).Within(0.001f));
+ }
+
+ [Test]
+ [TestCase(GraphicsUnit.Pixel, 0.5f, CoordinateSpace.World, CoordinateSpace.World, 10f, 10f)]
+ [TestCase(GraphicsUnit.Pixel, 0.5f, CoordinateSpace.World, CoordinateSpace.Page, 20f, 5f)]
+ [TestCase(GraphicsUnit.Pixel, 0.5f, CoordinateSpace.World, CoordinateSpace.Device, 40f, 10f)]
+ [TestCase(GraphicsUnit.Pixel, 0.5f, CoordinateSpace.Page, CoordinateSpace.Page, 10f, 10f)]
+ [TestCase(GraphicsUnit.Pixel, 0.5f, CoordinateSpace.Page, CoordinateSpace.World, 5f, 20f)]
+ [TestCase(GraphicsUnit.Pixel, 0.5f, CoordinateSpace.Page, CoordinateSpace.Device, 20f, 20f)]
+ [TestCase(GraphicsUnit.Pixel, 0.5f, CoordinateSpace.Device, CoordinateSpace.Device, 10f, 10f)]
+ [TestCase(GraphicsUnit.Pixel, 0.5f, CoordinateSpace.Device, CoordinateSpace.World, 2.5f, 10f)]
+ [TestCase(GraphicsUnit.Pixel, 0.5f, CoordinateSpace.Device, CoordinateSpace.Page, 5f, 5f)]
+ [TestCase(GraphicsUnit.Inch, 0.5f, CoordinateSpace.World, CoordinateSpace.World, 10f, 10f)]
+ [TestCase(GraphicsUnit.Inch, 0.5f, CoordinateSpace.World, CoordinateSpace.Page, 20f, 5f)]
+ [TestCase(GraphicsUnit.Inch, 0.5f, CoordinateSpace.World, CoordinateSpace.Device, 0.4166666f, 0.1041667f)]
+ [TestCase(GraphicsUnit.Inch, 0.5f, CoordinateSpace.Page, CoordinateSpace.Page, 10f, 10f)]
+ [TestCase(GraphicsUnit.Inch, 0.5f, CoordinateSpace.Page, CoordinateSpace.World, 5f, 20f)]
+ [TestCase(GraphicsUnit.Inch, 0.5f, CoordinateSpace.Page, CoordinateSpace.Device, 0.2083333f, 0.2083333f)]
+ [TestCase(GraphicsUnit.Inch, 0.5f, CoordinateSpace.Device, CoordinateSpace.Device, 10f, 10f)]
+ [TestCase(GraphicsUnit.Inch, 0.5f, CoordinateSpace.Device, CoordinateSpace.World, 240f, 960f)]
+ [TestCase(GraphicsUnit.Inch, 0.5f, CoordinateSpace.Device, CoordinateSpace.Page, 480f, 480f)]
+ [TestCase(GraphicsUnit.Millimeter, 0.5f, CoordinateSpace.World, CoordinateSpace.World, 10f, 10f)]
+ [TestCase(GraphicsUnit.Millimeter, 0.5f, CoordinateSpace.World, CoordinateSpace.Page, 20f, 5f)]
+ [TestCase(GraphicsUnit.Millimeter, 0.5f, CoordinateSpace.World, CoordinateSpace.Device, 10.58333f, 2.645833f)]
+ [TestCase(GraphicsUnit.Millimeter, 0.5f, CoordinateSpace.Page, CoordinateSpace.Page, 10f, 10f)]
+ [TestCase(GraphicsUnit.Millimeter, 0.5f, CoordinateSpace.Page, CoordinateSpace.World, 5f, 20f)]
+ [TestCase(GraphicsUnit.Millimeter, 0.5f, CoordinateSpace.Page, CoordinateSpace.Device, 5.291666f, 5.291666f)]
+ [TestCase(GraphicsUnit.Millimeter, 0.5f, CoordinateSpace.Device, CoordinateSpace.Device, 10f, 10f)]
+ [TestCase(GraphicsUnit.Millimeter, 0.5f, CoordinateSpace.Device, CoordinateSpace.World, 9.448818f, 37.79527f)]
+ [TestCase(GraphicsUnit.Millimeter, 0.5f, CoordinateSpace.Device, CoordinateSpace.Page, 18.89764f, 18.89764f)]
+ [TestCase(GraphicsUnit.Point, 0.5f, CoordinateSpace.World, CoordinateSpace.World, 10f, 10f)]
+ [TestCase(GraphicsUnit.Point, 0.5f, CoordinateSpace.World, CoordinateSpace.Page, 20f, 5f)]
+ [TestCase(GraphicsUnit.Point, 0.5f, CoordinateSpace.World, CoordinateSpace.Device, 30f, 7.5f)]
+ [TestCase(GraphicsUnit.Point, 0.5f, CoordinateSpace.Page, CoordinateSpace.Page, 10f, 10f)]
+ [TestCase(GraphicsUnit.Point, 0.5f, CoordinateSpace.Page, CoordinateSpace.World, 5f, 20f)]
+ [TestCase(GraphicsUnit.Point, 0.5f, CoordinateSpace.Page, CoordinateSpace.Device, 15f, 15f)]
+ [TestCase(GraphicsUnit.Point, 0.5f, CoordinateSpace.Device, CoordinateSpace.Device, 10f, 10f)]
+ [TestCase(GraphicsUnit.Point, 0.5f, CoordinateSpace.Device, CoordinateSpace.World, 3.333333f, 13.33333f)]
+ [TestCase(GraphicsUnit.Point, 0.5f, CoordinateSpace.Device, CoordinateSpace.Page, 6.666666f, 6.666666f)]
+ [TestCase(GraphicsUnit.Document, 0.5f, CoordinateSpace.World, CoordinateSpace.World, 10f, 10f)]
+ [TestCase(GraphicsUnit.Document, 0.5f, CoordinateSpace.World, CoordinateSpace.Page, 20f, 5f)]
+ [TestCase(GraphicsUnit.Document, 0.5f, CoordinateSpace.World, CoordinateSpace.Device, 125f, 31.25f)]
+ [TestCase(GraphicsUnit.Document, 0.5f, CoordinateSpace.Page, CoordinateSpace.Page, 10f, 10f)]
+ [TestCase(GraphicsUnit.Document, 0.5f, CoordinateSpace.Page, CoordinateSpace.World, 5f, 20f)]
+ [TestCase(GraphicsUnit.Document, 0.5f, CoordinateSpace.Page, CoordinateSpace.Device, 62.5f, 62.5f)]
+ [TestCase(GraphicsUnit.Document, 0.5f, CoordinateSpace.Device, CoordinateSpace.Device, 10f, 10f)]
+ [TestCase(GraphicsUnit.Document, 0.5f, CoordinateSpace.Device, CoordinateSpace.World, 0.8f, 3.2f)]
+ [TestCase(GraphicsUnit.Document, 0.5f, CoordinateSpace.Device, CoordinateSpace.Page, 1.6f, 1.6f)]
+ public void Method_TransformPoints(GraphicsUnit unit, float scale, CoordinateSpace destination, CoordinateSpace source, float expectedX, float expectedY)
+ {
+ var points = new PointF[] { new(10, 10) };
+
+ using var bitmap = new Bitmap(50, 50);
+ using var g = Graphics.FromImage(bitmap);
+
+ g.ScaleTransform(0.5f, 2);
+
+ g.PageUnit = unit;
+ g.PageScale = scale;
+
+ g.TransformPoints(destination, source, points);
+ Assert.Multiple(() =>
+ {
+ Assert.That(points[0].X, Is.EqualTo(expectedX).Within(0.001f));
+ Assert.That(points[0].Y, Is.EqualTo(expectedY).Within(0.001f));
+ });
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/test/Common/PenUnitTest.cs b/test/Common/PenUnitTest.cs
new file mode 100644
index 0000000..055c2ac
--- /dev/null
+++ b/test/Common/PenUnitTest.cs
@@ -0,0 +1,132 @@
+using GeneXus.Drawing.Drawing2D;
+
+namespace GeneXus.Drawing.Test;
+
+internal class PenUnitTest
+{
+ [SetUp]
+ public void Setup()
+ {
+ }
+
+ [Test]
+ public void Constructor_Color()
+ {
+ var color = Color.Red;
+
+ using var pen = new Pen(color);
+ Assert.Multiple(() =>
+ {
+ Assert.That(pen.Color, Is.EqualTo(color));
+ Assert.That(pen.Width, Is.EqualTo(1.0f));
+ Assert.That(pen.Alignment, Is.EqualTo(PenAlignment.Center));
+ Assert.That(pen.CompoundArray, Is.Empty);
+ Assert.That(pen.DashCap, Is.EqualTo(DashCap.Flat));
+ Assert.That(pen.DashOffset, Is.EqualTo(0));
+ Assert.That(pen.DashPattern, Is.Empty);
+ Assert.That(pen.PenType, Is.EqualTo(PenType.SolidColor));
+ Assert.That(pen.StartCap, Is.EqualTo(LineCap.Flat));
+ Assert.That(pen.EndCap, Is.EqualTo(LineCap.Flat));
+ Assert.That(pen.LineJoin, Is.EqualTo(LineJoin.Miter));
+ Assert.That(pen.MiterLimit, Is.EqualTo(10));
+ Assert.That(pen.Transform.IsIdentity, Is.True);
+ });
+ }
+
+ [Test]
+ public void Constructor_ColorAndWidth()
+ {
+ var color = Color.Green;
+ var width = 5.0f;
+
+ using var pen = new Pen(color, width);
+ Assert.Multiple(() =>
+ {
+ Assert.That(pen.Color, Is.EqualTo(color));
+ Assert.That(pen.Width, Is.EqualTo(width));
+ });
+ }
+
+ [Test]
+ public void Constructor_Brush()
+ {
+ var color = Color.Green;
+ var brush = new SolidBrush(color);
+ var width = 5.0f;
+
+ using var pen = new Pen(brush, width);
+ Assert.Multiple(() =>
+ {
+ Assert.That(pen.Color, Is.EqualTo(color));
+ Assert.That(pen.Width, Is.EqualTo(width));
+ });
+ }
+
+ [Test]
+ public void Constructor_Properties()
+ {
+ var color = Color.Red;
+ var width = 10.0f;
+ var alignment = PenAlignment.Left;
+ var brush = new LinearGradientBrush(new Point(0, 0), new Point(1, 1), Color.Black, Color.White);
+ var compund = new[] { 1.0f, 2.0f };
+ var dashCap = DashCap.Round;
+ var dashOffset = 3.0f;
+ var dashPattern = new[] { 0.0f, 5.0f, 10.0f };
+ var lineCap1 = LineCap.Round;
+ var lineCap2 = LineCap.Square;
+ var lineJoin = LineJoin.Bevel;
+ var mitterLimit = 5.0f;
+ var matrix = new Matrix(1, 2, 3, 4, 5, 6);
+
+ using var pen = new Pen(Color.Green, 5.0f)
+ {
+ Color = color,
+ Width = width,
+ Alignment = alignment,
+ Brush = brush,
+ CompoundArray = compund,
+ DashCap = dashCap,
+ DashOffset = dashOffset,
+ DashPattern = dashPattern,
+ StartCap = lineCap1,
+ EndCap = lineCap2,
+ LineJoin = lineJoin,
+ MiterLimit = mitterLimit,
+ Transform = matrix,
+ };
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(pen.Color, Is.EqualTo(color));
+ Assert.That(pen.Width, Is.EqualTo(width));
+ Assert.That(pen.Alignment, Is.EqualTo(alignment));
+ Assert.That(pen.Brush, Is.TypeOf());
+ Assert.That(pen.PenType, Is.EqualTo(PenType.LinearGradient));
+ Assert.That(pen.CompoundArray, Is.EqualTo(compund));
+ Assert.That(pen.DashCap, Is.EqualTo(dashCap));
+ Assert.That(pen.DashOffset, Is.EqualTo(dashOffset));
+ Assert.That(pen.DashPattern, Is.EqualTo(dashPattern));
+ Assert.That(pen.StartCap, Is.EqualTo(lineCap1));
+ Assert.That(pen.EndCap, Is.EqualTo(lineCap2));
+ Assert.That(pen.LineJoin, Is.EqualTo(lineJoin));
+ Assert.That(pen.MiterLimit, Is.EqualTo(mitterLimit));
+ Assert.That(pen.Transform, Is.EqualTo(matrix));
+ });
+ }
+
+ [Test]
+ public void Method_Clone()
+ {
+ using var pen1 = new Pen(Color.Blue, 2.0f);
+ using var pen2 = pen1.Clone() as Pen;
+ Assert.Multiple(() =>
+ {
+ Assert.That(pen1, Is.Not.Null);
+ Assert.That(pen1, Is.Not.SameAs(pen2));
+ Assert.That(pen2.Color, Is.EqualTo(pen1.Color));
+ Assert.That(pen2.Width, Is.EqualTo(pen1.Width));
+ Assert.That(pen2.Brush, Is.TypeOf());
+ });
+ }
+}
\ No newline at end of file
diff --git a/test/Common/PointFUnitTest.cs b/test/Common/PointFUnitTest.cs
index cab61f0..8b45a67 100644
--- a/test/Common/PointFUnitTest.cs
+++ b/test/Common/PointFUnitTest.cs
@@ -1,3 +1,5 @@
+using System.Numerics;
+
namespace GeneXus.Drawing.Test;
internal class PointFUnitTest
@@ -67,6 +69,18 @@ public void Constructor_Int()
});
}
+ [Test]
+ public void Constructor_Vector2()
+ {
+ var vector = new Vector2(10f, 20f);
+ var point = new PointF(vector);
+ Assert.Multiple(() =>
+ {
+ Assert.That(point.X, Is.EqualTo(vector.X));
+ Assert.That(point.Y, Is.EqualTo(vector.Y));
+ });
+ }
+
[Test]
public void Operator_Equality()
{
@@ -151,38 +165,14 @@ public void Method_Offset_Point()
}
[Test]
- public void Static_Method_Ceiling()
+ public void Method_ToVector2()
{
- var point = new PointF(10.4f, 20.6f);
- var result = PointF.Ceiling(point);
- Assert.Multiple(() =>
- {
- Assert.That(result.X, Is.EqualTo(11f));
- Assert.That(result.Y, Is.EqualTo(21f));
- });
- }
-
- [Test]
- public void Static_Method_Truncate()
- {
- var point = new PointF(10.9f, 20.6f);
- var result = PointF.Truncate(point);
- Assert.Multiple(() =>
- {
- Assert.That(result.X, Is.EqualTo(10f));
- Assert.That(result.Y, Is.EqualTo(20f));
- });
- }
-
- [Test]
- public void Static_Method_Round()
- {
- var point = new PointF(10.4f, 20.6f);
- var result = PointF.Round(point);
+ var point = new PointF(10f, 20f);
+ var vector = point.ToVector2();
Assert.Multiple(() =>
{
- Assert.That(result.X, Is.EqualTo(10f));
- Assert.That(result.Y, Is.EqualTo(21f));
+ Assert.That(vector.X, Is.EqualTo(point.X));
+ Assert.That(vector.Y, Is.EqualTo(point.Y));
});
}
}
diff --git a/test/Common/PointUnitTest.cs b/test/Common/PointUnitTest.cs
index 6503dff..8fa7bb5 100644
--- a/test/Common/PointUnitTest.cs
+++ b/test/Common/PointUnitTest.cs
@@ -150,4 +150,40 @@ public void Method_Offset_Point()
Assert.That(point.Y, Is.EqualTo(15f));
});
}
+
+ [Test]
+ public void Static_Method_Ceiling()
+ {
+ var point = new PointF(10.4f, 20.6f);
+ var result = Point.Ceiling(point);
+ Assert.Multiple(() =>
+ {
+ Assert.That(result.X, Is.EqualTo(11));
+ Assert.That(result.Y, Is.EqualTo(21));
+ });
+ }
+
+ [Test]
+ public void Static_Method_Truncate()
+ {
+ var point = new PointF(10.9f, 20.6f);
+ var result = Point.Truncate(point);
+ Assert.Multiple(() =>
+ {
+ Assert.That(result.X, Is.EqualTo(10));
+ Assert.That(result.Y, Is.EqualTo(20));
+ });
+ }
+
+ [Test]
+ public void Static_Method_Round()
+ {
+ var point = new PointF(10.4f, 20.6f);
+ var result = Point.Round(point);
+ Assert.Multiple(() =>
+ {
+ Assert.That(result.X, Is.EqualTo(10));
+ Assert.That(result.Y, Is.EqualTo(21));
+ });
+ }
}
\ No newline at end of file
diff --git a/test/Common/RegionUnitTest.cs b/test/Common/RegionUnitTest.cs
new file mode 100644
index 0000000..ee92b6c
--- /dev/null
+++ b/test/Common/RegionUnitTest.cs
@@ -0,0 +1,280 @@
+using System;
+using System.IO;
+using GeneXus.Drawing.Drawing2D;
+
+namespace GeneXus.Drawing.Test;
+
+internal class RegionUnitTest
+{
+ private static readonly string IMAGE_PATH = Path.Combine(
+ Directory.GetParent(Environment.CurrentDirectory).Parent.FullName,
+ "res", "images");
+
+ [Test]
+ public void Constructor_Default()
+ {
+ using var region = new Region();
+ Assert.Multiple(() =>
+ {
+ Assert.That(region, Is.Not.Null);
+ Assert.That(region.GetBounds(null), Is.EqualTo(RectangleF.Empty));
+ });
+ }
+
+ [Test]
+ public void Constructor_RectangleF()
+ {
+ var rect = new RectangleF(0, 0, 100, 100);
+
+ using var region = new Region(rect);
+ Assert.Multiple(() =>
+ {
+ Assert.That(region, Is.Not.Null);
+ Assert.That(region.GetBounds(null), Is.EqualTo(rect));
+ });
+ }
+
+ [Test]
+ public void Constructor_Rectangle()
+ {
+ var rectF = new RectangleF(0, 0, 100, 100);
+ var rectI = RectangleF.Truncate(rectF);
+
+ using var region = new Region(rectI);
+ Assert.Multiple(() =>
+ {
+ Assert.That(region, Is.Not.Null);
+ Assert.That(region.GetBounds(null), Is.EqualTo(rectF));
+ });
+ }
+
+ [Test]
+ public void Constructor_GraphicsPath()
+ {
+ var rect = new RectangleF(0, 0, 100, 100);
+
+ var path = new GraphicsPath();
+ path.AddRectangle(rect);
+
+ using var region = new Region(path);
+ Assert.Multiple(() =>
+ {
+ Assert.That(region, Is.Not.Null);
+ Assert.That(region.GetBounds(null), Is.EqualTo(rect));
+ });
+ }
+
+ [Test]
+ public void Constructor_RegionData()
+ {
+ var rect = new RectangleF(0, 0, 100, 100);
+
+ using var initialRegion = new Region(rect);
+ var data = initialRegion.GetRegionData();
+
+ using var region = new Region(data);
+ Assert.Multiple(() =>
+ {
+ Assert.That(region, Is.Not.Null);
+ Assert.That(region.GetBounds(null), Is.EqualTo(rect));
+ });
+ }
+
+ [Test]
+ public void Method_Clone()
+ {
+ var rect = new RectangleF(0, 0, 100, 100);
+
+ using var region1 = new Region(rect);
+ using var region2 = region1.Clone();
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(region2, Is.Not.Null);
+ Assert.That(region2, Is.TypeOf());
+ Assert.That(region2, Is.Not.SameAs(region1));
+ Assert.That(region2.GetBounds(null), Is.EqualTo(rect));
+ });
+ }
+
+ [Test]
+ public void Method_Complement_RectangleF()
+ {
+ using var region1 = new Region(new RectangleF(0, 0, 100, 100));
+ using var region2 = new Region(new RectangleF(50, 50, 100, 100));
+
+ region1.Complement(region2);
+ Assert.That(region1.IsVisible(50, 50), Is.False);
+ }
+
+ [Test]
+ public void Method_Complement_GraphicsPath()
+ {
+ var path = new GraphicsPath();
+ path.AddRectangle(new RectangleF(50, 50, 100, 100));
+
+ using var region = new Region(new RectangleF(0, 0, 100, 100));
+
+ region.Complement(path);
+ Assert.That(region.IsVisible(50, 50), Is.False);
+ }
+
+ [Test]
+ public void Method_Intersect_Region()
+ {
+ using var region1 = new Region(new RectangleF(0, 0, 100, 100));
+ using var region2 = new Region(new RectangleF(50, 50, 100, 100));
+
+ region1.Intersect(region2);
+ Assert.That(region1.IsVisible(75, 75), Is.True);
+ }
+
+ [Test]
+ public void Method_Intersect_RectangleF()
+ {
+ using var region = new Region(new RectangleF(0, 0, 100, 100));
+
+ region.Intersect(new RectangleF(50, 50, 100, 100));
+ Assert.That(region.IsVisible(75, 75), Is.True);
+ }
+
+ [Test]
+ public void Method_Intersect_GraphicsPath()
+ {
+ var path = new GraphicsPath();
+ path.AddRectangle(new RectangleF(50, 50, 100, 100));
+
+ using var region = new Region(new RectangleF(0, 0, 100, 100));
+
+ region.Intersect(path);
+ Assert.That(region.IsVisible(75, 75), Is.True);
+ }
+
+ [Test]
+ public void Method_Union_Region()
+ {
+ using var region1 = new Region(new RectangleF(0, 0, 100, 100));
+ using var region2 = new Region(new RectangleF(50, 50, 100, 100));
+
+ region1.Union(region2);
+ Assert.That(region1.IsVisible(75, 75), Is.True);
+ Assert.That(region1.IsVisible(25, 25), Is.True);
+ }
+
+ [Test]
+ public void Method_Union_RectangleF()
+ {
+ using var region = new Region(new RectangleF(0, 0, 100, 100));
+
+ region.Union(new RectangleF(50, 50, 100, 100));
+ Assert.That(region.IsVisible(75, 75), Is.True);
+ Assert.That(region.IsVisible(25, 25), Is.True);
+ }
+
+ [Test]
+ public void Method_Union_GraphicsPath()
+ {
+ var path = new GraphicsPath();
+ path.AddRectangle(new RectangleF(50, 50, 100, 100));
+
+ using var region = new Region(new RectangleF(0, 0, 100, 100));
+
+ region.Union(path);
+ Assert.That(region.IsVisible(75, 75), Is.True);
+ Assert.That(region.IsVisible(25, 25), Is.True);
+ }
+
+ [Test]
+ public void Method_Xor_Region()
+ {
+ using var region1 = new Region(new RectangleF(0, 0, 100, 100));
+ using var region2 = new Region(new RectangleF(50, 50, 100, 100));
+
+ region1.Xor(region2);
+ Assert.That(region1.IsVisible(75, 75), Is.False);
+ Assert.That(region1.IsVisible(25, 25), Is.True);
+ Assert.That(region1.IsVisible(125, 125), Is.True);
+ }
+
+ [Test]
+ public void Method_Xor_RectangleF()
+ {
+ using var region = new Region(new RectangleF(0, 0, 100, 100));
+
+ region.Xor(new RectangleF(50, 50, 100, 100));
+ Assert.That(region.IsVisible(75, 75), Is.False);
+ Assert.That(region.IsVisible(25, 25), Is.True);
+ Assert.That(region.IsVisible(125, 125), Is.True);
+ }
+
+ [Test]
+ public void Method_Xor_GraphicsPath()
+ {
+ var path = new GraphicsPath();
+ path.AddRectangle(new RectangleF(50, 50, 100, 100));
+
+ using var region = new Region(new RectangleF(0, 0, 100, 100));
+
+ region.Xor(path);
+ Assert.That(region.IsVisible(75, 75), Is.False);
+ Assert.That(region.IsVisible(25, 25), Is.True);
+ Assert.That(region.IsVisible(125, 125), Is.True);
+ }
+
+ [Test]
+ public void Method_GetBounds_Graphics()
+ {
+ var filePath = Path.Combine(IMAGE_PATH, "Sample.png");
+ using var image = Image.FromFile(filePath);
+ using var graphics = Graphics.FromImage(image);
+
+ var rect = new RectangleF(0, 0, 100, 100);
+ using var region = new Region(rect);
+
+ var bounds = region.GetBounds(graphics);
+ Assert.That(bounds, Is.EqualTo(rect));
+ }
+
+ [Test]
+ public void Method_GetRegionData()
+ {
+ using var region = new Region(new RectangleF(0, 0, 100, 100));
+ var data = region.GetRegionData();
+
+ Assert.That(data, Is.Not.Null);
+ }
+
+ [Test]
+ public void Method_Transform_Matrix()
+ {
+ using var region = new Region(new RectangleF(0, 0, 100, 100));
+
+ var matrix = new Matrix();
+ matrix.Translate(50, 50);
+
+ region.Transform(matrix);
+ Assert.That(region.IsVisible(75, 75), Is.True);
+ }
+
+ [Test]
+ public void Method_Translate()
+ {
+ using var region = new Region(new RectangleF(0, 0, 100, 100));
+
+ region.Translate(50, 50);
+ Assert.That(region.IsVisible(75, 75), Is.True);
+ }
+
+ [Test]
+ public void Method_GetRegionScans()
+ {
+ var rect = new RectangleF(0, 0, 100, 100);
+
+ using var region = new Region(rect);
+ var scans = region.GetRegionScans(new Matrix());
+
+ Assert.That(scans, Is.Not.Null);
+ Assert.That(scans.Length, Is.EqualTo(1));
+ Assert.That(scans[0], Is.EqualTo(rect));
+ }
+}
\ No newline at end of file
diff --git a/test/Common/SolidBrushUnitTest.cs b/test/Common/SolidBrushUnitTest.cs
new file mode 100644
index 0000000..4f74d4d
--- /dev/null
+++ b/test/Common/SolidBrushUnitTest.cs
@@ -0,0 +1,53 @@
+using System.IO;
+
+namespace GeneXus.Drawing.Test;
+
+internal class SolidBrushUnitTest
+{
+ [SetUp]
+ public void Setup()
+ {
+ }
+
+ [Test]
+ [TestCase("Red")]
+ [TestCase("Green")]
+ [TestCase("Blue")]
+ public void Constructor_Color(string name)
+ {
+ var color = Color.FromName(name);
+
+ using var brush = new SolidBrush(color);
+ Assert.Multiple(() =>
+ {
+ Assert.That(brush.Color, Is.EqualTo(color));
+
+ string path = Path.Combine("brush", "solid", $"Color{name}.png");
+ float similarity = Utils.CompareImage(path, brush, true);
+ Assert.That(similarity, Is.GreaterThan(0.9));
+ });
+ }
+
+ [Test]
+ public void Property_Color()
+ {
+ var color = Color.Blue;
+
+ using var brush = new SolidBrush(Color.Red);
+ brush.Color = color;
+ Assert.That(brush.Color, Is.EqualTo(color));
+ }
+
+ [Test]
+ public void Method_Clone()
+ {
+ using var brush1 = new SolidBrush(Color.Blue);
+ using var brush2 = brush1.Clone() as SolidBrush;
+ Assert.Multiple(() =>
+ {
+ Assert.That(brush2, Is.Not.Null);
+ Assert.That(brush2, Is.Not.SameAs(brush1));
+ Assert.That(brush2.Color, Is.EqualTo(brush1.Color));
+ });
+ }
+}
\ No newline at end of file
diff --git a/test/Common/StringFormatUnitTest.cs b/test/Common/StringFormatUnitTest.cs
new file mode 100644
index 0000000..406786d
--- /dev/null
+++ b/test/Common/StringFormatUnitTest.cs
@@ -0,0 +1,80 @@
+namespace GeneXus.Drawing.Test;
+
+internal class StringFormatUnitTest
+{
+ [SetUp]
+ public void Setup()
+ {
+ }
+
+ [Test]
+ public void Constructor_Default()
+ {
+ var format = new StringFormat();
+ Assert.Multiple(() =>
+ {
+ Assert.That(format.FormatFlags, Is.EqualTo((StringFormatFlags)0));
+ Assert.That(format.Alignment, Is.EqualTo(StringAlignment.Near));
+ Assert.That(format.LineAlignment, Is.EqualTo(StringAlignment.Near));
+ Assert.That(format.Trimming, Is.EqualTo(StringTrimming.None));
+ Assert.That(format.DigitSubstitutionMethod, Is.EqualTo(StringDigitSubstitute.None));
+ Assert.That(format.DigitSubstitutionLanguage, Is.EqualTo(0));
+ });
+ }
+
+ [Test]
+ public void Constructor_FormatFlags()
+ {
+ var format = new StringFormat(StringFormatFlags.DirectionVertical | StringFormatFlags.DirectionRightToLeft);
+ Assert.Multiple(() =>
+ {
+ Assert.That(format.FormatFlags.HasFlag(StringFormatFlags.DirectionVertical));
+ Assert.That(format.FormatFlags.HasFlag(StringFormatFlags.DirectionRightToLeft));
+ Assert.That(!format.FormatFlags.HasFlag(StringFormatFlags.NoClip));
+ Assert.That(format.DigitSubstitutionLanguage, Is.EqualTo(0));
+ });
+ }
+
+ [Test]
+ public void Constructor_FormatFlagsAndLanguage()
+ {
+ var format = new StringFormat(StringFormatFlags.DirectionVertical | StringFormatFlags.DirectionRightToLeft, 1033);
+ Assert.Multiple(() =>
+ {
+ Assert.That(format.FormatFlags.HasFlag(StringFormatFlags.DirectionVertical));
+ Assert.That(format.FormatFlags.HasFlag(StringFormatFlags.DirectionRightToLeft));
+ Assert.That(!format.FormatFlags.HasFlag(StringFormatFlags.NoClip));
+ Assert.That(format.DigitSubstitutionLanguage, Is.EqualTo(1033));
+ });
+ }
+
+ [Test]
+ public void Method_GetSetDigitSubstitution()
+ {
+ var format = new StringFormat();
+
+ format.SetDigitSubstitution(1033, StringDigitSubstitute.Traditional);
+ Assert.Multiple(() =>
+ {
+ Assert.That(format.DigitSubstitutionLanguage, Is.EqualTo(1033));
+ Assert.That(format.DigitSubstitutionMethod, Is.EqualTo(StringDigitSubstitute.Traditional));
+ });
+ }
+
+ [Test]
+ public void Method_GetSetTabStops()
+ {
+ var format = new StringFormat();
+
+ float expTabOffset = 2.0f;
+ float[] expTabStops = { 4.0f, 8.0f, 12.0f };
+ format.SetTabStops(expTabOffset, expTabStops);
+
+ float[] tabStops = format.GetTabStops(out float tabOffset);
+ Assert.Multiple(() =>
+ {
+ Assert.That(tabOffset, Is.EqualTo(expTabOffset));
+ Assert.That(tabStops, Is.EqualTo(expTabStops));
+ });
+ }
+}
\ No newline at end of file
diff --git a/test/Common/TextureBrushUnitTest.cs b/test/Common/TextureBrushUnitTest.cs
new file mode 100644
index 0000000..7154f4c
--- /dev/null
+++ b/test/Common/TextureBrushUnitTest.cs
@@ -0,0 +1,61 @@
+using System;
+using System.IO;
+using GeneXus.Drawing.Drawing2D;
+
+namespace GeneXus.Drawing.Test;
+
+internal class TextureBrushUnitTest
+{
+ private static readonly string IMAGE_PATH = Path.Combine(
+ Directory.GetParent(Environment.CurrentDirectory).Parent.FullName,
+ "res", "images");
+
+ [SetUp]
+ public void Setup()
+ {
+ }
+
+ [Test]
+ [TestCase(WrapMode.Tile)]
+ [TestCase(WrapMode.Clamp)]
+ public void Constructor_BitmapRectWrap(WrapMode mode)
+ {
+ var filePath = Path.Combine(IMAGE_PATH, "Sample.png");
+ using var bitmap = new Bitmap(filePath);
+ var rect = new RectangleF(15, 15, 20, 20);
+
+ var matrix = new Matrix();
+ matrix.Rotate(45);
+ matrix.Translate(10, 10);
+
+ using var brush = new TextureBrush(bitmap, mode, rect) { Transform = matrix };
+ Assert.Multiple(() =>
+ {
+ Assert.That(brush.Image, Is.EqualTo(bitmap));
+ Assert.That(brush.WrapMode, Is.EqualTo(mode));
+ Assert.That(brush.Transform, Is.EqualTo(matrix));
+
+ string path = Path.Combine("brush", "textured", $"Mode{mode}.png");
+ float similarity = Utils.CompareImage(path, brush, true);
+ Assert.That(similarity, Is.GreaterThan(0.9));
+ });
+ }
+
+ [Test]
+ public void Method_Clone()
+ {
+ var filePath = Path.Combine(IMAGE_PATH, "Sample.png");
+ using var bitmap = new Bitmap(filePath);
+
+ using var brush1 = new TextureBrush(bitmap, WrapMode.Tile, new Rectangle(0, 0, 100, 50)) { Transform = new Matrix(1, 2, 3, 4, 5, 6) };
+ using var brush2 = brush1.Clone() as TextureBrush;
+ Assert.Multiple(() =>
+ {
+ Assert.That(brush2, Is.Not.Null);
+ Assert.That(brush2, Is.Not.SameAs(brush1));
+ Assert.That(brush2.Image, Is.EqualTo(brush1.Image));
+ Assert.That(brush2.WrapMode, Is.EqualTo(brush1.WrapMode));
+ Assert.That(brush2.Transform, Is.EqualTo(brush1.Transform));
+ });
+ }
+}
\ No newline at end of file
diff --git a/test/Common/Utils.cs b/test/Common/Utils.cs
new file mode 100644
index 0000000..d39b4b0
--- /dev/null
+++ b/test/Common/Utils.cs
@@ -0,0 +1,123 @@
+using System;
+using System.IO;
+using System.Linq;
+
+namespace GeneXus.Drawing.Test;
+
+internal abstract class Utils
+{
+ private static readonly string IMAGE_PATH = Path.Combine(
+ Directory.GetParent(Environment.CurrentDirectory).Parent.FullName,
+ "res", "images");
+
+ private static readonly double MAX_COLOR_DISTANCE = GetColorDistance(
+ Color.FromArgb(0, 0, 0, 0),
+ Color.FromArgb(255, 255, 255, 255));
+
+ public static RectangleF GetBoundingRectangle(PointF[] points)
+ {
+ if (points == null || points.Length == 0)
+ throw new ArgumentException("The points array cannot be null or empty.", nameof(points));
+
+ float xMin = points.Min(pt => pt.X);
+ float xMax = points.Max(pt => pt.X);
+ float yMin = points.Min(pt => pt.Y);
+ float yMax = points.Max(pt => pt.Y);
+ return new(xMin, yMin, xMax - xMin, yMax - yMin);
+ }
+
+ public static PointF GetCenterPoint(PointF[] points)
+ {
+ float xCenter = points.Average(pt => pt.X);
+ float yCenter = points.Average(pt => pt.Y);
+ return new(xCenter, yCenter);
+ }
+
+ public static double GetColorDistance(Color color1, Color color2)
+ {
+ double aDiff = Math.Pow(color1.A - color2.A, 2);
+ double rDiff = Math.Pow(color1.R - color2.R, 2);
+ double gDiff = Math.Pow(color1.G - color2.G, 2);
+ double bDiff = Math.Pow(color1.B - color2.B, 2);
+ return Math.Sqrt(aDiff + rDiff + gDiff + bDiff);
+ }
+
+ public static Color GetAverageColor(Bitmap bm, int x, int y, int radius)
+ {
+ int aSum = 0, rSum = 0, gSum = 0, bSum = 0, count = 0;
+ for (int dy = -radius; dy <= radius; dy++)
+ {
+ for (int dx = -radius; dx <= radius; dx++)
+ {
+ int nx = x + dx;
+ int ny = y + dy;
+
+ if (nx < 0 || ny < 0 || nx >= bm.Width || ny >= bm.Height)
+ continue;
+
+ Color color = bm.GetPixel(nx, ny);
+ rSum += color.R;
+ gSum += color.G;
+ bSum += color.B;
+ aSum += color.A;
+ count++;
+ }
+ }
+ return Color.FromArgb(aSum / count, rSum / count, gSum / count, bSum / count);
+ }
+
+ public static float GetSimilarity(Bitmap bm1, Bitmap bm2, double tolerance = 0.1, int window = 3)
+ {
+ float hits = 0f; // compare pixel by pixel considering the average in a box of window size
+ if (bm1.Size == bm2.Size)
+ {
+ int radius = window / 2;
+ double threshold = tolerance * MAX_COLOR_DISTANCE;
+
+ for (int i = 0; i < bm1.Width; i++)
+ {
+ for (int j = 0; j < bm1.Height; j++)
+ {
+ Color avg1 = GetAverageColor(bm1, i, j, radius);
+ Color avg2 = GetAverageColor(bm2, i, j, radius);
+
+ hits += GetColorDistance(avg1, avg2) < threshold ? 1 : 0;
+ }
+ }
+ }
+ return hits / (bm1.Width * bm1.Height);
+ }
+
+ public static float CompareImage(string filename, Brush brush, bool save = false)
+ {
+ string filepath = Path.Combine(IMAGE_PATH, filename);
+ using var im = Image.FromFile(filepath);
+ using var bm = new Bitmap(im);
+
+ var gu = GraphicsUnit.Pixel;
+ using var bg = new Bitmap(bm.Width, bm.Height);
+ using var g = Graphics.FromImage(bg);
+ g.FillRectangle(brush, bg.GetBounds(ref gu));
+
+ return CompareImage(filename, bg, save);
+ }
+
+ public static float CompareImage(string filename, Bitmap bg, bool save = false)
+ {
+ string filepath = Path.Combine(IMAGE_PATH, filename);
+ using var im = Image.FromFile(filepath);
+ using var bm = new Bitmap(im);
+
+ if (save)
+ {
+ string savepath = Path.Combine(IMAGE_PATH, ".out", filename);
+
+ string dirpath = Path.GetDirectoryName(savepath);
+ if (!Directory.Exists(dirpath)) Directory.CreateDirectory(dirpath);
+
+ bg.Save(savepath);
+ }
+
+ return GetSimilarity(bg, bm);
+ }
+}
\ No newline at end of file
diff --git a/test/Common/res/images/brush/hatch/StyleBackwardDiagonal.png b/test/Common/res/images/brush/hatch/StyleBackwardDiagonal.png
new file mode 100644
index 0000000..f8f5d0e
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StyleBackwardDiagonal.png differ
diff --git a/test/Common/res/images/brush/hatch/StyleCross.png b/test/Common/res/images/brush/hatch/StyleCross.png
new file mode 100644
index 0000000..f1d8714
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StyleCross.png differ
diff --git a/test/Common/res/images/brush/hatch/StyleDarkDownwardDiagonal.png b/test/Common/res/images/brush/hatch/StyleDarkDownwardDiagonal.png
new file mode 100644
index 0000000..08e4329
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StyleDarkDownwardDiagonal.png differ
diff --git a/test/Common/res/images/brush/hatch/StyleDarkHorizontal.png b/test/Common/res/images/brush/hatch/StyleDarkHorizontal.png
new file mode 100644
index 0000000..ef01405
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StyleDarkHorizontal.png differ
diff --git a/test/Common/res/images/brush/hatch/StyleDarkUpwardDiagonal.png b/test/Common/res/images/brush/hatch/StyleDarkUpwardDiagonal.png
new file mode 100644
index 0000000..21a7c7b
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StyleDarkUpwardDiagonal.png differ
diff --git a/test/Common/res/images/brush/hatch/StyleDarkVertical.png b/test/Common/res/images/brush/hatch/StyleDarkVertical.png
new file mode 100644
index 0000000..5a62da4
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StyleDarkVertical.png differ
diff --git a/test/Common/res/images/brush/hatch/StyleDashedDownwardDiagonal.png b/test/Common/res/images/brush/hatch/StyleDashedDownwardDiagonal.png
new file mode 100644
index 0000000..6c7f590
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StyleDashedDownwardDiagonal.png differ
diff --git a/test/Common/res/images/brush/hatch/StyleDashedHorizontal.png b/test/Common/res/images/brush/hatch/StyleDashedHorizontal.png
new file mode 100644
index 0000000..9fd499e
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StyleDashedHorizontal.png differ
diff --git a/test/Common/res/images/brush/hatch/StyleDashedUpwardDiagonal.png b/test/Common/res/images/brush/hatch/StyleDashedUpwardDiagonal.png
new file mode 100644
index 0000000..201ddb0
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StyleDashedUpwardDiagonal.png differ
diff --git a/test/Common/res/images/brush/hatch/StyleDashedVertical.png b/test/Common/res/images/brush/hatch/StyleDashedVertical.png
new file mode 100644
index 0000000..d2bd5e6
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StyleDashedVertical.png differ
diff --git a/test/Common/res/images/brush/hatch/StyleDiagonalBrick.png b/test/Common/res/images/brush/hatch/StyleDiagonalBrick.png
new file mode 100644
index 0000000..1873ba2
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StyleDiagonalBrick.png differ
diff --git a/test/Common/res/images/brush/hatch/StyleDiagonalCross.png b/test/Common/res/images/brush/hatch/StyleDiagonalCross.png
new file mode 100644
index 0000000..3190855
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StyleDiagonalCross.png differ
diff --git a/test/Common/res/images/brush/hatch/StyleDivot.png b/test/Common/res/images/brush/hatch/StyleDivot.png
new file mode 100644
index 0000000..f687e74
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StyleDivot.png differ
diff --git a/test/Common/res/images/brush/hatch/StyleDottedDiamond.png b/test/Common/res/images/brush/hatch/StyleDottedDiamond.png
new file mode 100644
index 0000000..9beb47e
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StyleDottedDiamond.png differ
diff --git a/test/Common/res/images/brush/hatch/StyleDottedGrid.png b/test/Common/res/images/brush/hatch/StyleDottedGrid.png
new file mode 100644
index 0000000..35e1b10
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StyleDottedGrid.png differ
diff --git a/test/Common/res/images/brush/hatch/StyleForwardDiagonal.png b/test/Common/res/images/brush/hatch/StyleForwardDiagonal.png
new file mode 100644
index 0000000..9b56c7d
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StyleForwardDiagonal.png differ
diff --git a/test/Common/res/images/brush/hatch/StyleHorizontal.png b/test/Common/res/images/brush/hatch/StyleHorizontal.png
new file mode 100644
index 0000000..0256fd3
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StyleHorizontal.png differ
diff --git a/test/Common/res/images/brush/hatch/StyleHorizontalBrick.png b/test/Common/res/images/brush/hatch/StyleHorizontalBrick.png
new file mode 100644
index 0000000..c5f4c0f
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StyleHorizontalBrick.png differ
diff --git a/test/Common/res/images/brush/hatch/StyleLargeCheckerBoard.png b/test/Common/res/images/brush/hatch/StyleLargeCheckerBoard.png
new file mode 100644
index 0000000..99b9a09
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StyleLargeCheckerBoard.png differ
diff --git a/test/Common/res/images/brush/hatch/StyleLargeConfetti.png b/test/Common/res/images/brush/hatch/StyleLargeConfetti.png
new file mode 100644
index 0000000..c21d9d5
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StyleLargeConfetti.png differ
diff --git a/test/Common/res/images/brush/hatch/StyleLightDownwardDiagonal.png b/test/Common/res/images/brush/hatch/StyleLightDownwardDiagonal.png
new file mode 100644
index 0000000..95dd3b5
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StyleLightDownwardDiagonal.png differ
diff --git a/test/Common/res/images/brush/hatch/StyleLightHorizontal.png b/test/Common/res/images/brush/hatch/StyleLightHorizontal.png
new file mode 100644
index 0000000..e277a27
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StyleLightHorizontal.png differ
diff --git a/test/Common/res/images/brush/hatch/StyleLightUpwardDiagonal.png b/test/Common/res/images/brush/hatch/StyleLightUpwardDiagonal.png
new file mode 100644
index 0000000..b38f564
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StyleLightUpwardDiagonal.png differ
diff --git a/test/Common/res/images/brush/hatch/StyleLightVertical.png b/test/Common/res/images/brush/hatch/StyleLightVertical.png
new file mode 100644
index 0000000..c0b64e2
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StyleLightVertical.png differ
diff --git a/test/Common/res/images/brush/hatch/StyleNarrowHorizontal.png b/test/Common/res/images/brush/hatch/StyleNarrowHorizontal.png
new file mode 100644
index 0000000..9018442
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StyleNarrowHorizontal.png differ
diff --git a/test/Common/res/images/brush/hatch/StyleNarrowVertical.png b/test/Common/res/images/brush/hatch/StyleNarrowVertical.png
new file mode 100644
index 0000000..2e207b3
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StyleNarrowVertical.png differ
diff --git a/test/Common/res/images/brush/hatch/StyleOutlinedDiamond.png b/test/Common/res/images/brush/hatch/StyleOutlinedDiamond.png
new file mode 100644
index 0000000..80ed5ab
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StyleOutlinedDiamond.png differ
diff --git a/test/Common/res/images/brush/hatch/StylePercent05.png b/test/Common/res/images/brush/hatch/StylePercent05.png
new file mode 100644
index 0000000..243429f
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StylePercent05.png differ
diff --git a/test/Common/res/images/brush/hatch/StylePercent10.png b/test/Common/res/images/brush/hatch/StylePercent10.png
new file mode 100644
index 0000000..8a85349
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StylePercent10.png differ
diff --git a/test/Common/res/images/brush/hatch/StylePercent20.png b/test/Common/res/images/brush/hatch/StylePercent20.png
new file mode 100644
index 0000000..6e678b9
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StylePercent20.png differ
diff --git a/test/Common/res/images/brush/hatch/StylePercent25.png b/test/Common/res/images/brush/hatch/StylePercent25.png
new file mode 100644
index 0000000..6b4816a
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StylePercent25.png differ
diff --git a/test/Common/res/images/brush/hatch/StylePercent30.png b/test/Common/res/images/brush/hatch/StylePercent30.png
new file mode 100644
index 0000000..5dbbe1a
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StylePercent30.png differ
diff --git a/test/Common/res/images/brush/hatch/StylePercent40.png b/test/Common/res/images/brush/hatch/StylePercent40.png
new file mode 100644
index 0000000..4d36f93
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StylePercent40.png differ
diff --git a/test/Common/res/images/brush/hatch/StylePercent50.png b/test/Common/res/images/brush/hatch/StylePercent50.png
new file mode 100644
index 0000000..267317a
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StylePercent50.png differ
diff --git a/test/Common/res/images/brush/hatch/StylePercent60.png b/test/Common/res/images/brush/hatch/StylePercent60.png
new file mode 100644
index 0000000..0312c41
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StylePercent60.png differ
diff --git a/test/Common/res/images/brush/hatch/StylePercent70.png b/test/Common/res/images/brush/hatch/StylePercent70.png
new file mode 100644
index 0000000..ec2c77f
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StylePercent70.png differ
diff --git a/test/Common/res/images/brush/hatch/StylePercent75.png b/test/Common/res/images/brush/hatch/StylePercent75.png
new file mode 100644
index 0000000..4585644
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StylePercent75.png differ
diff --git a/test/Common/res/images/brush/hatch/StylePercent80.png b/test/Common/res/images/brush/hatch/StylePercent80.png
new file mode 100644
index 0000000..66dce34
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StylePercent80.png differ
diff --git a/test/Common/res/images/brush/hatch/StylePercent90.png b/test/Common/res/images/brush/hatch/StylePercent90.png
new file mode 100644
index 0000000..c5211a7
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StylePercent90.png differ
diff --git a/test/Common/res/images/brush/hatch/StylePlaid.png b/test/Common/res/images/brush/hatch/StylePlaid.png
new file mode 100644
index 0000000..e6bb641
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StylePlaid.png differ
diff --git a/test/Common/res/images/brush/hatch/StyleShingle.png b/test/Common/res/images/brush/hatch/StyleShingle.png
new file mode 100644
index 0000000..24af73c
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StyleShingle.png differ
diff --git a/test/Common/res/images/brush/hatch/StyleSmallCheckerBoard.png b/test/Common/res/images/brush/hatch/StyleSmallCheckerBoard.png
new file mode 100644
index 0000000..25bda14
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StyleSmallCheckerBoard.png differ
diff --git a/test/Common/res/images/brush/hatch/StyleSmallConfetti.png b/test/Common/res/images/brush/hatch/StyleSmallConfetti.png
new file mode 100644
index 0000000..e87427b
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StyleSmallConfetti.png differ
diff --git a/test/Common/res/images/brush/hatch/StyleSmallGrid.png b/test/Common/res/images/brush/hatch/StyleSmallGrid.png
new file mode 100644
index 0000000..a385555
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StyleSmallGrid.png differ
diff --git a/test/Common/res/images/brush/hatch/StyleSolidDiamond.png b/test/Common/res/images/brush/hatch/StyleSolidDiamond.png
new file mode 100644
index 0000000..d0cd11e
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StyleSolidDiamond.png differ
diff --git a/test/Common/res/images/brush/hatch/StyleSphere.png b/test/Common/res/images/brush/hatch/StyleSphere.png
new file mode 100644
index 0000000..aa1324e
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StyleSphere.png differ
diff --git a/test/Common/res/images/brush/hatch/StyleTrellis.png b/test/Common/res/images/brush/hatch/StyleTrellis.png
new file mode 100644
index 0000000..88fcd4d
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StyleTrellis.png differ
diff --git a/test/Common/res/images/brush/hatch/StyleVertical.png b/test/Common/res/images/brush/hatch/StyleVertical.png
new file mode 100644
index 0000000..1d00816
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StyleVertical.png differ
diff --git a/test/Common/res/images/brush/hatch/StyleWave.png b/test/Common/res/images/brush/hatch/StyleWave.png
new file mode 100644
index 0000000..90515d8
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StyleWave.png differ
diff --git a/test/Common/res/images/brush/hatch/StyleWeave.png b/test/Common/res/images/brush/hatch/StyleWeave.png
new file mode 100644
index 0000000..00ff989
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StyleWeave.png differ
diff --git a/test/Common/res/images/brush/hatch/StyleWideDownwardDiagonal.png b/test/Common/res/images/brush/hatch/StyleWideDownwardDiagonal.png
new file mode 100644
index 0000000..cdf87a7
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StyleWideDownwardDiagonal.png differ
diff --git a/test/Common/res/images/brush/hatch/StyleWideUpwardDiagonal.png b/test/Common/res/images/brush/hatch/StyleWideUpwardDiagonal.png
new file mode 100644
index 0000000..9c2b1cb
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StyleWideUpwardDiagonal.png differ
diff --git a/test/Common/res/images/brush/hatch/StyleZigzag.png b/test/Common/res/images/brush/hatch/StyleZigzag.png
new file mode 100644
index 0000000..f4ce0f6
Binary files /dev/null and b/test/Common/res/images/brush/hatch/StyleZigzag.png differ
diff --git a/test/Common/res/images/brush/linear/Angle0.png b/test/Common/res/images/brush/linear/Angle0.png
new file mode 100644
index 0000000..bd74fcf
Binary files /dev/null and b/test/Common/res/images/brush/linear/Angle0.png differ
diff --git a/test/Common/res/images/brush/linear/Angle45.png b/test/Common/res/images/brush/linear/Angle45.png
new file mode 100644
index 0000000..a4f0004
Binary files /dev/null and b/test/Common/res/images/brush/linear/Angle45.png differ
diff --git a/test/Common/res/images/brush/linear/Angle75.png b/test/Common/res/images/brush/linear/Angle75.png
new file mode 100644
index 0000000..39ab949
Binary files /dev/null and b/test/Common/res/images/brush/linear/Angle75.png differ
diff --git a/test/Common/res/images/brush/linear/ModeBackwardDiagonal.png b/test/Common/res/images/brush/linear/ModeBackwardDiagonal.png
new file mode 100644
index 0000000..a1660a1
Binary files /dev/null and b/test/Common/res/images/brush/linear/ModeBackwardDiagonal.png differ
diff --git a/test/Common/res/images/brush/linear/ModeForwardDiagonal.png b/test/Common/res/images/brush/linear/ModeForwardDiagonal.png
new file mode 100644
index 0000000..a4f0004
Binary files /dev/null and b/test/Common/res/images/brush/linear/ModeForwardDiagonal.png differ
diff --git a/test/Common/res/images/brush/linear/ModeHorizontal.png b/test/Common/res/images/brush/linear/ModeHorizontal.png
new file mode 100644
index 0000000..bd74fcf
Binary files /dev/null and b/test/Common/res/images/brush/linear/ModeHorizontal.png differ
diff --git a/test/Common/res/images/brush/linear/ModeVertical.png b/test/Common/res/images/brush/linear/ModeVertical.png
new file mode 100644
index 0000000..9551a46
Binary files /dev/null and b/test/Common/res/images/brush/linear/ModeVertical.png differ
diff --git a/test/Common/res/images/brush/linear/angle90.png b/test/Common/res/images/brush/linear/angle90.png
new file mode 100644
index 0000000..9551a46
Binary files /dev/null and b/test/Common/res/images/brush/linear/angle90.png differ
diff --git a/test/Common/res/images/brush/path/Circle.png b/test/Common/res/images/brush/path/Circle.png
new file mode 100644
index 0000000..87a7fb8
Binary files /dev/null and b/test/Common/res/images/brush/path/Circle.png differ
diff --git a/test/Common/res/images/brush/path/CircleRecentered.png b/test/Common/res/images/brush/path/CircleRecentered.png
new file mode 100644
index 0000000..86d66e9
Binary files /dev/null and b/test/Common/res/images/brush/path/CircleRecentered.png differ
diff --git a/test/Common/res/images/brush/path/CircleRefocused.png b/test/Common/res/images/brush/path/CircleRefocused.png
new file mode 100644
index 0000000..4bcd2c1
Binary files /dev/null and b/test/Common/res/images/brush/path/CircleRefocused.png differ
diff --git a/test/Common/res/images/brush/path/CircleTransform.png b/test/Common/res/images/brush/path/CircleTransform.png
new file mode 100644
index 0000000..1ebc169
Binary files /dev/null and b/test/Common/res/images/brush/path/CircleTransform.png differ
diff --git a/test/Common/res/images/brush/path/TriangleBlended.png b/test/Common/res/images/brush/path/TriangleBlended.png
new file mode 100644
index 0000000..b702e98
Binary files /dev/null and b/test/Common/res/images/brush/path/TriangleBlended.png differ
diff --git a/test/Common/res/images/brush/path/TriangleClamp.png b/test/Common/res/images/brush/path/TriangleClamp.png
new file mode 100644
index 0000000..7463b22
Binary files /dev/null and b/test/Common/res/images/brush/path/TriangleClamp.png differ
diff --git a/test/Common/res/images/brush/path/TriangleInterpolated.png b/test/Common/res/images/brush/path/TriangleInterpolated.png
new file mode 100644
index 0000000..edc4881
Binary files /dev/null and b/test/Common/res/images/brush/path/TriangleInterpolated.png differ
diff --git a/test/Common/res/images/brush/path/TriangleRefocused.png b/test/Common/res/images/brush/path/TriangleRefocused.png
new file mode 100644
index 0000000..0a31976
Binary files /dev/null and b/test/Common/res/images/brush/path/TriangleRefocused.png differ
diff --git a/test/Common/res/images/brush/path/TriangleSurrounded.png b/test/Common/res/images/brush/path/TriangleSurrounded.png
new file mode 100644
index 0000000..fe08985
Binary files /dev/null and b/test/Common/res/images/brush/path/TriangleSurrounded.png differ
diff --git a/test/Common/res/images/brush/path/TriangleTile.png b/test/Common/res/images/brush/path/TriangleTile.png
new file mode 100644
index 0000000..846cbb3
Binary files /dev/null and b/test/Common/res/images/brush/path/TriangleTile.png differ
diff --git a/test/Common/res/images/brush/solid/ColorBlue.png b/test/Common/res/images/brush/solid/ColorBlue.png
new file mode 100644
index 0000000..ae82c5d
Binary files /dev/null and b/test/Common/res/images/brush/solid/ColorBlue.png differ
diff --git a/test/Common/res/images/brush/solid/ColorGreen.png b/test/Common/res/images/brush/solid/ColorGreen.png
new file mode 100644
index 0000000..3be5560
Binary files /dev/null and b/test/Common/res/images/brush/solid/ColorGreen.png differ
diff --git a/test/Common/res/images/brush/solid/ColorRed.png b/test/Common/res/images/brush/solid/ColorRed.png
new file mode 100644
index 0000000..cc01cf1
Binary files /dev/null and b/test/Common/res/images/brush/solid/ColorRed.png differ
diff --git a/test/Common/res/images/brush/textured/ModeClamp.png b/test/Common/res/images/brush/textured/ModeClamp.png
new file mode 100644
index 0000000..994687f
Binary files /dev/null and b/test/Common/res/images/brush/textured/ModeClamp.png differ
diff --git a/test/Common/res/images/brush/textured/ModeTile.png b/test/Common/res/images/brush/textured/ModeTile.png
new file mode 100644
index 0000000..75e9806
Binary files /dev/null and b/test/Common/res/images/brush/textured/ModeTile.png differ
diff --git a/test/Common/res/images/graphics/ClipExclude.png b/test/Common/res/images/graphics/ClipExclude.png
new file mode 100644
index 0000000..77c0ba9
Binary files /dev/null and b/test/Common/res/images/graphics/ClipExclude.png differ
diff --git a/test/Common/res/images/graphics/ClipIntersect.png b/test/Common/res/images/graphics/ClipIntersect.png
new file mode 100644
index 0000000..cfa39a5
Binary files /dev/null and b/test/Common/res/images/graphics/ClipIntersect.png differ
diff --git a/test/Common/res/images/graphics/ClipTranslate.png b/test/Common/res/images/graphics/ClipTranslate.png
new file mode 100644
index 0000000..57c8bd9
Binary files /dev/null and b/test/Common/res/images/graphics/ClipTranslate.png differ
diff --git a/test/Common/res/images/graphics/DrawArc.png b/test/Common/res/images/graphics/DrawArc.png
new file mode 100644
index 0000000..3889991
Binary files /dev/null and b/test/Common/res/images/graphics/DrawArc.png differ
diff --git a/test/Common/res/images/graphics/DrawBezier.png b/test/Common/res/images/graphics/DrawBezier.png
new file mode 100644
index 0000000..2f12657
Binary files /dev/null and b/test/Common/res/images/graphics/DrawBezier.png differ
diff --git a/test/Common/res/images/graphics/DrawClosedCurve.png b/test/Common/res/images/graphics/DrawClosedCurve.png
new file mode 100644
index 0000000..bd23bce
Binary files /dev/null and b/test/Common/res/images/graphics/DrawClosedCurve.png differ
diff --git a/test/Common/res/images/graphics/DrawCurve.png b/test/Common/res/images/graphics/DrawCurve.png
new file mode 100644
index 0000000..378780c
Binary files /dev/null and b/test/Common/res/images/graphics/DrawCurve.png differ
diff --git a/test/Common/res/images/graphics/DrawEllipse.png b/test/Common/res/images/graphics/DrawEllipse.png
new file mode 100644
index 0000000..3e27119
Binary files /dev/null and b/test/Common/res/images/graphics/DrawEllipse.png differ
diff --git a/test/Common/res/images/graphics/DrawImage.png b/test/Common/res/images/graphics/DrawImage.png
new file mode 100644
index 0000000..2ea4327
Binary files /dev/null and b/test/Common/res/images/graphics/DrawImage.png differ
diff --git a/test/Common/res/images/graphics/DrawLine.png b/test/Common/res/images/graphics/DrawLine.png
new file mode 100644
index 0000000..4031ace
Binary files /dev/null and b/test/Common/res/images/graphics/DrawLine.png differ
diff --git a/test/Common/res/images/graphics/DrawPath.png b/test/Common/res/images/graphics/DrawPath.png
new file mode 100644
index 0000000..a3bb786
Binary files /dev/null and b/test/Common/res/images/graphics/DrawPath.png differ
diff --git a/test/Common/res/images/graphics/DrawPie.png b/test/Common/res/images/graphics/DrawPie.png
new file mode 100644
index 0000000..7b3f057
Binary files /dev/null and b/test/Common/res/images/graphics/DrawPie.png differ
diff --git a/test/Common/res/images/graphics/DrawPolygon.png b/test/Common/res/images/graphics/DrawPolygon.png
new file mode 100644
index 0000000..709a297
Binary files /dev/null and b/test/Common/res/images/graphics/DrawPolygon.png differ
diff --git a/test/Common/res/images/graphics/DrawRectangle.png b/test/Common/res/images/graphics/DrawRectangle.png
new file mode 100644
index 0000000..0751861
Binary files /dev/null and b/test/Common/res/images/graphics/DrawRectangle.png differ
diff --git a/test/Common/res/images/graphics/DrawString.png b/test/Common/res/images/graphics/DrawString.png
new file mode 100644
index 0000000..eb9d26c
Binary files /dev/null and b/test/Common/res/images/graphics/DrawString.png differ
diff --git a/test/Common/res/images/graphics/FillClosedCurve.png b/test/Common/res/images/graphics/FillClosedCurve.png
new file mode 100644
index 0000000..5318226
Binary files /dev/null and b/test/Common/res/images/graphics/FillClosedCurve.png differ
diff --git a/test/Common/res/images/graphics/FillEllipse.png b/test/Common/res/images/graphics/FillEllipse.png
new file mode 100644
index 0000000..79e9c72
Binary files /dev/null and b/test/Common/res/images/graphics/FillEllipse.png differ
diff --git a/test/Common/res/images/graphics/FillPath.png b/test/Common/res/images/graphics/FillPath.png
new file mode 100644
index 0000000..5fa17d0
Binary files /dev/null and b/test/Common/res/images/graphics/FillPath.png differ
diff --git a/test/Common/res/images/graphics/FillPie.png b/test/Common/res/images/graphics/FillPie.png
new file mode 100644
index 0000000..3b356ee
Binary files /dev/null and b/test/Common/res/images/graphics/FillPie.png differ
diff --git a/test/Common/res/images/graphics/FillPolygon.png b/test/Common/res/images/graphics/FillPolygon.png
new file mode 100644
index 0000000..c1dccea
Binary files /dev/null and b/test/Common/res/images/graphics/FillPolygon.png differ
diff --git a/test/Common/res/images/graphics/FillRectangle.png b/test/Common/res/images/graphics/FillRectangle.png
new file mode 100644
index 0000000..c2bf254
Binary files /dev/null and b/test/Common/res/images/graphics/FillRectangle.png differ
diff --git a/test/Common/res/images/graphics/FillRegion.png b/test/Common/res/images/graphics/FillRegion.png
new file mode 100644
index 0000000..d8c2b51
Binary files /dev/null and b/test/Common/res/images/graphics/FillRegion.png differ
diff --git a/test/Common/res/images/graphics/RenderingOrigin_00-00.png b/test/Common/res/images/graphics/RenderingOrigin_00-00.png
new file mode 100644
index 0000000..3e02571
Binary files /dev/null and b/test/Common/res/images/graphics/RenderingOrigin_00-00.png differ
diff --git a/test/Common/res/images/graphics/RenderingOrigin_25-25.png b/test/Common/res/images/graphics/RenderingOrigin_25-25.png
new file mode 100644
index 0000000..5c8f26b
Binary files /dev/null and b/test/Common/res/images/graphics/RenderingOrigin_25-25.png differ