System.Drawing drop-in
Egami.SystemDrawing gives code written for System.Drawing.Common its API on every OS, in managed code: Bitmap, Image,
Icon, pixel formats, LockBits, the codecs and their encoder parameters, frames and property items. System.Drawing.Common
runs only on Windows since .NET 6; with this package the same code runs on Linux, macOS and in containers, drawn by Egami.
The package is a preview (1.0.0-preview.3). This release has bitmaps, pixel access, codecs, icons, and Graphics with pens,
brushes, paths, transforms, clipping, regions and images, fonts and text, metafiles, effects, off-screen buffers and the image
animator.
Moving code over
Replace the System.Drawing.Common package reference with Egami.SystemDrawing. With implicit usings on, the package adds
System.Drawing and Egami.SystemDrawing as implicit usings, so most files compile unchanged; files that name
System.Drawing.Imaging types change that line to using Egami.SystemDrawing.Imaging;. Set EgamiSystemDrawingImplicitUsings
to false in the project file to leave the implicit usings out.
Color, Point, Size, Rectangle, their float forms, KnownColor, SystemColors and ColorTranslator are .NET's own
System.Drawing.Primitives types, which every OS has, so values pass between your code, other libraries and Egami unchanged.
Everything else (Bitmap, Image, PixelFormat, ImageFormat and the rest) is defined by the package with
System.Drawing's names, members, overloads, defaults and exceptions: ArgumentException for "Parameter is not valid",
ExternalException for GDI+'s generic error, OutOfMemoryException from Image.FromFile for a file it cannot read.
The package is source-compatible, not binary-compatible: your code recompiles against it, while a DLL built against System.Drawing.Common keeps using System.Drawing.Common.
// The same code as with System.Drawing.Common: only the using lines name Egami.SystemDrawing.
using Bitmap bitmap = new(64, 32);
for (int x = 0; x < 64; x++)
{
bitmap.SetPixel(x, 16, Color.FromArgb(255, x * 4, 0, 255 - (x * 4)));
}
Color middle = bitmap.GetPixel(32, 16); // ARGB(255, 128, 0, 127)
bitmap.SetResolution(300, 300);
bitmap.RotateFlip(RotateFlipType.Rotate90FlipNone);
bitmap.Save(output, ImageFormat.Png);
GraphicsUnit unit = GraphicsUnit.Inch;
RectangleF bounds = bitmap.GetBounds(ref unit); // 32 by 64, in pixels
GraphicsUnit and RotateFlipType (with System.Drawing's shared values, such as RotateNoneFlipY equal to Rotate180FlipX)
live in Egami.SystemDrawing, as they do in System.Drawing.
Bitmaps and pixel formats
A Bitmap holds its pixels in GDI+'s own memory layout for each PixelFormat: rows of the format's bits, each padded to a
multiple of four bytes. The 14 bitmap formats are all there, and pixels convert exactly as GDI+ converts them, measured on
Windows for every 8-bit value and every color and alpha pair:
- 5 and 6-bit channels (
Format16bppRgb555,Format16bppRgb565,Format16bppArgb1555) truncate when written and repeat their high bits when read; 1555's alpha bit is set from alpha 128. Format32bppPArgbpremultiplies with rounding and divides out through GDI+'s 16-bit reciprocal of alpha.Format32bppRgbkeeps the fourth byte it is given and reads as opaque.Format48bppRgb,Format64bppArgbandFormat64bppPArgbhold linear light from 0 to 8192 through the sRGB curve, as GDI+ does, so 8-bit colors round-trip.- Indexed formats look up their palette;
SetPixelon them throwsInvalidOperationException, andFormat16bppGrayScalebitmaps exist butGetPixelandSetPixelthrow, as in GDI+.
Image.GetPixelFormatSize, IsAlphaPixelFormat, IsExtendedPixelFormat and IsCanonicalPixelFormat read the format's bits,
and Image.Flags holds ImageFlags values: alpha, color space, and for a file, read-only and a stated resolution.
// Every GDI+ pixel format, with GDI+'s conversions: 5-bit channels truncate when written and repeat their high bits when read.
using Bitmap rgb555 = new(1, 1, PixelFormat.Format16bppRgb555);
rgb555.SetPixel(0, 0, Color.FromArgb(10, 20, 30));
Color read = rgb555.GetPixel(0, 0); // ARGB(255, 8, 16, 24)
// 48 and 64-bit formats hold linear light from 0 to 8192, as in GDI+.
using Bitmap deep = new(1, 1, PixelFormat.Format48bppRgb);
deep.SetPixel(0, 0, Color.FromArgb(255, 200, 100, 50));
int bits = Image.GetPixelFormatSize(deep.PixelFormat); // 48
bool alpha = Image.IsAlphaPixelFormat(PixelFormat.Format64bppArgb); // true
ImageFlags flags = (ImageFlags)new Bitmap(1, 1, PixelFormat.Format32bppArgb).Flags; // HasAlpha
Clone(rect, format) copies an area into another format, ConvertFormat converts in place, MakeTransparent keys out a color
(the bottom-left pixel's by default) into 32-bit ARGB, RotateFlip turns and mirrors, and GetThumbnailImage makes a 32-bit
premultiplied copy. new Bitmap(image, size) draws another image at a size, bilinearly, as GDI+'s DrawImage does. A bitmap made
with new Bitmap(width, height, stride, format, scan0) reads and writes the caller's memory in place.
Locking pixels
LockBits with an ImageLockMode returns a BitmapData: where the first locked row is, the bytes from one row to the next, and
the size and format. Locked in the bitmap's own format, the pointer is into the bitmap's memory. Locked in another format, it is
a converted copy, written back on UnlockBits when the lock writes. ImageLockMode.UserInputBuffer locks into a buffer the
caller's BitmapData points to. While a bitmap is locked, GetPixel, SetPixel and a second lock throw ExternalException, as
in GDI+.
// Locked in its own format, the bitmap's pixels are its own memory; in another format they are a converted copy that
// goes back into the bitmap on UnlockBits.
BitmapData data = bitmap.LockBits(new Rectangle(0, 0, 16, 8), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
byte[] row = new byte[data.Stride];
for (int y = 0; y < data.Height; y++)
{
Marshal.Copy(data.Scan0 + (y * data.Stride), row, 0, row.Length);
for (int x = 0; x < data.Width; x++)
{
row[(x * 4) + 2] = 255; // red
}
Marshal.Copy(row, 0, data.Scan0 + (y * data.Stride), row.Length);
}
bitmap.UnlockBits(data);
Palettes
Indexed bitmaps start with GDI+'s default palettes: black and white, the 16 VGA colors, or for 8 bits the VGA colors, 24
transparent entries and a 6 by 6 by 6 cube. Image.Palette hands out a copy as a ColorPalette; set it back to change it. Its
Flags hold PaletteFlags. new ColorPalette(PaletteType) builds GDI+'s fixed halftone palettes, and
ColorPalette.CreateOptimalPalette builds one from a bitmap's colors with Egami's palette builder.
Converting to an indexed format maps each color as GDI+ does, through a table of 16 levels a channel: Clone uses GDI+'s
halftone palettes (2, 16 or 224 colors), a lock uses the format's default palette, and ConvertFormat the palette or
PaletteType it is given. Every DitherType maps to the nearest color; Egami does not dither yet.
// Indexed bitmaps keep GDI+'s default palettes; Palette hands out a copy to change and set back.
using Bitmap indexed = new(4, 4, PixelFormat.Format8bppIndexed);
ColorPalette palette = indexed.Palette;
palette.Entries[0] = Color.Navy;
indexed.Palette = palette;
bool halftone = (palette.Flags & (int)PaletteFlags.Halftone) != 0; // true
// ConvertFormat maps to a fixed palette, and Clone to an indexed format uses GDI+'s halftone palettes.
using Bitmap photo = new(System.IO.Path.Combine(Images, "duck.png"));
photo.ConvertFormat(PixelFormat.Format8bppIndexed, DitherType.None, PaletteType.FixedHalftone252);
using Bitmap bilevel = photo.Clone(new Rectangle(0, 0, photo.Width, photo.Height), PixelFormat.Format1bppIndexed);
Loading and saving
Image.FromFile, Image.FromStream and the Bitmap constructors read through Egami's codecs, which read every format Egami
does, more than GDI+. Images come in the pixel format GDI+ gives each file type: 24-bit PNG and JPEG as Format24bppRgb,
palette PNG, GIF and BMP files as their indexed formats with their palettes, gray PNG and 16-bit gray TIFF as Format32bppArgb,
gray JPEG and 8-bit gray TIFF as Format8bppIndexed with a gray palette, plain 32-bit BMP as Format32bppRgb, 16-bit RGB TIFF
as Format48bppRgb, icons and animated GIFs as Format32bppArgb. The resolution is the file's, computed as GDI+ computes it
(3780 pixels a meter is 96.01199), or 96. RawFormat is the file's ImageFormat.
Save writes BMP, PNG, GIF, TIFF and JPEG, the encoders ImageCodecInfo.GetImageEncoders lists with GDI+'s class ids, MIME
types and ImageCodecFlags. An EncoderParameters holds EncoderParameter settings, by Encoder: Encoder.Quality sets
JPEG quality (75 by default), Encoder.Compression with an EncoderValue sets TIFF compression (LZW by default),
Encoder.ColorDepth writes 1, 4 or 8-bit TIFF, and Encoder.SaveFlag with SaveAdd writes a multipage TIFF.
EncoderParameterValueType says what type a setting's values have. GetEncoderParameterList lists what each encoder takes,
as GDI+ lists it.
// The encoders GDI+ lists, by MIME type, with encoder parameters for JPEG quality and TIFF compression.
ImageCodecInfo jpeg = ImageCodecInfo.GetImageEncoders().First(c => c.MimeType == "image/jpeg");
using EncoderParameters settings = new(1);
settings.Param[0] = new EncoderParameter(Encoder.Quality, 85L);
using MemoryStream file = new();
photo.Save(file, jpeg, settings);
file.Position = 0;
using Image loaded = Image.FromStream(file);
bool fromJpeg = loaded.RawFormat.Equals(ImageFormat.Jpeg); // true
PixelFormat format = loaded.PixelFormat; // Format24bppRgb
bool readOnly = ((ImageFlags)loaded.Flags).HasFlag(ImageFlags.ReadOnly); // true
ImageCodecFlags can = jpeg.Flags; // Encoder, Decoder, SupportBitmap, Builtin
EncoderParameterValueType type = settings.Param[0].ValueType; // ValueTypeLong
EncoderValue lzw = EncoderValue.CompressionLZW; // for Encoder.Compression and TIFF
BMP is written as GDI+ writes it, with the bitmap's own rows, so a 32-bit bitmap reads back as Format32bppRgb. GIF is written
with the default 8-bit palette.
Frames and properties
A multipage TIFF counts its pages in FrameDimension.Page and an animated GIF its frames in FrameDimension.Time;
GetFrameCount and SelectActiveFrame choose one. PropertyItems, GetPropertyItem, SetPropertyItem and
RemovePropertyItem hold what GDI+ reports as PropertyItem values: the EXIF tags, every tag of a TIFF's first directory, a
JPEG's quantization tables (0x5090 and 0x5091), a PNG's gamma and pixel size, and a GIF's frame delays, loop count, global
palette, background and transparent index and comment, each with its tag id, type and little-endian bytes.
// An animated GIF counts its frames in time; property items hold its delays (0x5100) and loop count (0x5101).
using Image animation = Image.FromFile(System.IO.Path.Combine(Images, "loop.gif"));
FrameDimension time = new(animation.FrameDimensionsList[0]); // FrameDimension.Time
int frames = animation.GetFrameCount(time);
PropertyItem delays = animation.GetPropertyItem(0x5100)!; // 4 bytes a frame, in hundredths of a second
animation.SelectActiveFrame(time, frames - 1);
// Thumbnails are 32-bit premultiplied.
using Image thumbnail = animation.GetThumbnailImage(16, 12, null, IntPtr.Zero);
GetThumbnailImage takes an Image.GetThumbnailImageAbort callback, which, as in GDI+, is never called.
Icons
Icon loads an ICO file and shows the image nearest the size asked for, 32 by 32 by default, chosen as System.Drawing chooses:
the smallest difference in width plus height, then the most bits a pixel. ToBitmap gives it as 32-bit ARGB, and Save writes
the file it came from. Image.FromFile on an icon takes the last image of the file, as GDI+'s decoder does.
// An icon shows the image nearest the size asked for, as System.Drawing chooses it, and saves the file it came from.
using Icon icon = new(System.IO.Path.Combine(Images, "app.ico"), 24, 24);
using Bitmap image = icon.ToBitmap(); // 24 by 24, 32-bit ARGB
using MemoryStream copy = new();
icon.Save(copy);
Drawing with Graphics
Graphics.FromImage draws on a bitmap of any pixel format GDI+ draws on; indexed formats throw ArgumentException, and 16-bit
gray and 16-bit ARGB 1555 throw ExternalException, as in GDI+. Each call draws at once, so Flush does nothing, and the bitmap
keeps what was drawn after Dispose; FlushIntention is accepted and changes nothing. As in GDI+, drawing goes through premultiplied ARGB with GDI+'s rounding, so a
translucent copy onto a format without alpha keeps the premultiplied color.
GDI+'s defaults are kept: shapes are aliased (SmoothingMode.None), pixel centers lie on whole coordinates
(PixelOffsetMode.Default; Half and HighQuality move them half a pixel, to Egami's own centers), images are sampled
bilinearly, and drawing blends (CompositingMode.SourceOver; SourceCopy replaces). CompositingQuality and QualityMode
values are kept but blend the same way. Settings read back as GDI+ stores them:
SmoothingMode.HighQuality reads as AntiAlias, HighSpeed and Default as None, InterpolationMode.Low as Bilinear and
High as HighQualityBicubic. PageUnit and PageScale scale page coordinates at the bitmap's resolution; with
GraphicsUnit.Display, the default, GDI+ ignores the page scale and so does the package.
// Draw on a bitmap as with System.Drawing: GDI+'s defaults are aliased shapes and whole-pixel centers.
using Bitmap bitmap = new(80, 60);
using (Graphics g = Graphics.FromImage(bitmap))
{
g.Clear(Color.White);
g.SmoothingMode = SmoothingMode.AntiAlias;
g.PageUnit = GraphicsUnit.Millimeter; // 1 mm is 96 / 25.4 pixels at the bitmap's 96 dpi
using Pen pen = new(Color.Navy, 0.5f);
g.DrawEllipse(pen, 2, 2, 15, 10);
}
Color corner = bitmap.GetPixel(0, 0); // white
// Egami's own setting: 32-bit ARGB bitmaps draw in place by default; Exact rounds blends exactly as GDI+ does, more slowly.
using (Graphics exact = Graphics.FromImage(bitmap))
{
exact.ArgbDrawing = ArgbDrawing.Exact; // or SystemDrawingOptions.ArgbDrawing for every Graphics made afterwards
exact.FillRectangle(Brushes.Gold, 60, 40, 10, 10);
}
Transforms and states
Matrix is a 3 by 2 affine transform with System.Drawing's members; MatrixOrder.Prepend, the default, applies a new
transform before the existing one. The world transform takes the same calls on Graphics, and TransformPoints maps points
between CoordinateSpace.World, Page and Device. Save and Restore keep the transform, page settings, clip and quality
settings; restoring a state drops every state saved after it. BeginContainer returns a GraphicsContainer and starts again from the defaults inside the
current transform and clip; with a destination and a source rectangle, the source is in the given unit and the container's own
coordinates are pixels, as in GDI+.
// Transforms prepend by default, so the last call applies first; Save and Restore bring settings back.
using Bitmap bitmap = new(64, 64);
using Graphics g = Graphics.FromImage(bitmap);
g.TranslateTransform(32, 32);
g.RotateTransform(45);
GraphicsState saved = g.Save();
g.ScaleTransform(2, 2);
PointF[] points = [new(1, 0)];
g.TransformPoints(CoordinateSpace.Device, CoordinateSpace.World, points); // (33.41, 33.41)
g.Restore(saved);
using Matrix matrix = new();
matrix.Rotate(90);
matrix.Translate(10, 0, MatrixOrder.Append);
float[] elements = matrix.Elements; // 0, 1, -1, 0, 10, 0
Clipping and regions
The clip is kept in device pixels, as GDI+ keeps it, so a later transform does not move it, and it holds the pixels whose
whole coordinates lie inside: closed at the top and left, open at the bottom and right, half a pixel in when the pixel offset is
set. SetClip takes a CombineMode. ClipBounds and VisibleClipBounds are its pixels' bounds in world coordinates. A Region keeps its shape as given;
GetRegionScans, IsEmpty, Equals and the bounds of a combined region are its pixels, rasterized the same way. GetRegionData
writes GDI+'s region format as a RegionData, with the checksum left as zero.
// The clip is kept in device pixels, as GDI+ keeps it: changing the transform afterwards does not move it.
using Bitmap bitmap = new(64, 48);
using Graphics g = Graphics.FromImage(bitmap);
g.SetClip(new Rectangle(10, 10, 20, 20));
g.ExcludeClip(new Rectangle(15, 15, 5, 5));
g.TranslateTransform(100, 0);
RectangleF bounds = g.ClipBounds; // (-90, 10, 20, 20) in the new world coordinates
g.ResetTransform();
using SolidBrush brush = new(Color.Red);
g.FillRectangle(brush, 0, 0, 64, 48); // paints only inside the clip
using Region region = new(new Rectangle(0, 0, 4, 4));
region.Union(new RectangleF(2.5f, 2.5f, 4, 4));
RectangleF[] scans = region.GetRegionScans(new Matrix()); // the pixels, as rectangles
Shapes
DrawLine, DrawRectangle, DrawEllipse, DrawArc, DrawPie, DrawPolygon, DrawBezier, DrawCurve, DrawClosedCurve,
DrawRoundedRectangle and their Fill forms take every System.Drawing overload, including the span ones. Arcs are measured on
the ellipse as GDI+ measures them, cardinal splines use GDI+'s tension (0.5 by default), and a pen is never thinner than a
pixel. Polygons and closed curves fill even-odd unless given FillMode.Winding. DrawPath and FillPath draw a
GraphicsPath.
// Lines, rectangles, arcs, pies, polygons, Beziers and cardinal splines, stroked with a pen or filled with a brush.
using Bitmap bitmap = new(64, 48);
using Graphics g = Graphics.FromImage(bitmap);
using Pen pen = new(Color.Black, 2);
using SolidBrush brush = new(Color.FromArgb(128, 255, 0, 0));
g.DrawLine(pen, 2, 2, 60, 2);
g.FillRectangle(brush, 4, 6, 20, 12);
g.DrawArc(pen, 30, 6, 30, 20, 0, 180);
g.FillPie(brush, 4, 20, 24, 24, 30, 120);
g.DrawCurve(pen, [new PointF(30, 40), new PointF(40, 30), new PointF(50, 44), new PointF(60, 34)], 0.5f);
g.FillPolygon(brush, [new PointF(40, 4), new PointF(48, 16), new PointF(32, 16)], FillMode.Winding);
Pens
A Pen has GDI+'s settings and checks: StartCap and EndCap take every LineCap, the anchors centered on the end;
LineJoin with MiterLimit (at least 1); a DashStyle whose DashPattern is in multiples of the width, with a DashCap and
DashOffset (a solid pen's DashPattern throws InvalidOperationException, and Custom keeps the pattern of the style
before it); CompoundArray strips; PenAlignment.Inset, the only alignment GDI+ draws differently from centered; and a
Transform that shapes the nib, so a pen scaled 3 by 1 draws three times as wide across vertical lines. CustomEndCap and
CustomStartCap take an AdjustableArrowCap or a CustomLineCap with a stroke path; as in GDI+, a CustomLineCap with a fill
path throws NotImplementedException. PenType says what kind of brush the pen paints with. Pens and SystemPens hold
one-unit pens of the named and system colors, which cannot be changed or disposed.
// Pens take GDI+'s caps, joins, dash styles (in multiples of the width), compound lines, inset alignment and arrow caps.
using Bitmap bitmap = new(80, 60);
using Graphics g = Graphics.FromImage(bitmap);
g.SmoothingMode = SmoothingMode.AntiAlias;
using Pen dashed = new(Color.Navy, 3) { DashStyle = DashStyle.DashDot, StartCap = LineCap.Round, EndCap = LineCap.ArrowAnchor };
g.DrawLine(dashed, 5, 10, 75, 10);
using Pen arrow = new(Color.DarkRed, 2) { CustomEndCap = new AdjustableArrowCap(4, 5) };
g.DrawLine(arrow, 5, 30, 70, 30);
using Pen inset = new(Color.Black, 4) { Alignment = PenAlignment.Inset, LineJoin = LineJoin.Round };
g.DrawRectangle(inset, 5, 40, 30, 15);
float[] pattern = dashed.DashPattern; // 3, 1, 1, 1
Brushes
HatchBrush draws the 53 HatchStyle patterns pixel for pixel as GDI+ draws them on a bitmap, from the device origin moved by
RenderingOrigin, including GDI+'s smoothed forward, backward and crossed diagonals. TextureBrush tiles an image (or part of
it) through its transform, by its WrapMode; given ImageAttributes, it takes their wrap mode, Clamp unless they set one.
LinearGradientBrush runs across a rectangle at an angle, a LinearGradientMode or between two points, with GDI+'s brush
transform: the far corners lie on the end colors, and the diagonal modes and a scaleable angle work in the rectangle stretched to
a square. Its Blend falls off from SetSigmaBellShape (511 samples of the normal curve, as GDI+ takes them) or
SetBlendTriangularShape, or a ColorBlend sets preset colors, after which Blend is null. PathGradientBrush runs from a
center color at the mean of the boundary's points out to the surrounding colors. Brushes and SystemBrushes hold solid
brushes of the named and system colors; as in System.Drawing, the color of one of the Brushes can be changed. Every brush
derives from Brush.
// Hatch, texture and gradient brushes, and the Brushes and Pens of the named colors.
using Bitmap bitmap = new(80, 60);
using Graphics g = Graphics.FromImage(bitmap);
using HatchBrush hatch = new(HatchStyle.DiagonalCross, Color.Black, Color.White);
g.FillRectangle(hatch, 0, 0, 40, 30);
using LinearGradientBrush linear = new(new Rectangle(40, 0, 40, 30), Color.Red, Color.Blue, LinearGradientMode.Horizontal);
linear.SetSigmaBellShape(0.5f);
g.FillRectangle(linear, 40, 0, 40, 30);
using PathGradientBrush radial = new([new PointF(0, 30), new PointF(40, 30), new PointF(20, 60)]) { CenterColor = Color.White, SurroundColors = [Color.Green] };
g.FillRectangle(radial, 0, 30, 40, 30);
using Bitmap tile = new(4, 4);
tile.SetPixel(0, 0, Color.Orange);
using TextureBrush texture = new(tile, WrapMode.TileFlipXY);
g.FillRectangle(texture, 40, 30, 40, 30);
g.DrawRectangle(Egami.SystemDrawing.Pens.Gray, 0, 0, 79, 59);
g.FillEllipse(Egami.SystemDrawing.Brushes.Gold, 36, 26, 8, 8);
Blend? blend = linear.Blend; // 511 factors: the bell, as GDI+ samples it
Paths
A GraphicsPath keeps GDI+'s points and PathPointType types, built as GDI+ builds them: lines, arcs, Beziers and curves join
the open figure (a repeated point is dropped), while rectangles, ellipses, pies, polygons and closed curves are figures of their
own. Arcs are cubic Beziers of 90 degrees of the ellipse's own angle from the start, the remainder last. PathPoints,
PathTypes and PathData hand out copies. Flatten, Widen and Warp (a WarpMode of perspective or bilinear, from four
points or three) replace the figures; Reverse keeps each segment's kind, the closes and the markers. GetBounds with a matrix
transforms the bounds' corners and grows them for a pen as GDI+ does, and IsVisible and IsOutlineVisible test the pixel a
point rounds to. GraphicsPathIterator walks figures, runs of one type and markers. A path also makes a Region, a clip, and a
path gradient's boundary.
// Paths keep GDI+'s points and point types; lines, arcs and curves join the open figure, closed shapes are their own.
using GraphicsPath path = new();
path.AddLine(0, 0, 10, 10);
path.AddArc(0, 0, 40, 20, 0, 90);
path.CloseFigure();
path.AddEllipse(50, 0, 20, 20);
byte[] types = path.PathTypes; // 0 1 1 3 3 3|128, then the ellipse's 13 points
RectangleF bounds = path.GetBounds();
bool inside = path.IsVisible(60, 10);
using GraphicsPathIterator iterator = new(path);
int figures = iterator.SubpathCount; // 2
path.Widen(new Pen(Color.Black, 2));
using Bitmap bitmap = new(80, 30);
using Graphics g = Graphics.FromImage(bitmap);
g.FillPath(Egami.SystemDrawing.Brushes.Teal, path);
g.SetClip(path);
Images
DrawImage(image, x, y) and DrawImageUnscaled draw an image at its physical size: its pixels at its resolution, in page
units at the bitmap's resolution. The other overloads stretch an image or part of it into a rectangle or a parallelogram, with
the source in any GraphicsUnit. As in GDI+, the edges fade where sampling reaches past the image, unless ImageAttributes
sets a wrap mode; ImageAttributes also applies color matrices (with a ColorMatrixFlag for grays), gamma, thresholds, color
keys, ColorMap remap tables (by ColorMapType) and ColorChannelFlag output channels, each for every ColorAdjustType or
one of them. A DrawImageAbort callback that returns true stops the draw with ExternalException. DrawIcon stretches an icon,
and DrawIconUnstretched draws it at its size.
// DrawImage at a point draws at the image's physical size; ImageAttributes adjust its colors and tile it.
using Bitmap source = new(8, 8);
source.SetResolution(48, 48); // half the 96 dpi of the bitmap drawn on, so it is drawn twice as large
using (Graphics s = Graphics.FromImage(source))
{
s.Clear(Color.Orange);
}
using Bitmap bitmap = new(64, 48);
using Graphics g = Graphics.FromImage(bitmap);
g.DrawImage(source, 0, 0); // 16 by 16 pixels
g.InterpolationMode = InterpolationMode.NearestNeighbor;
using ImageAttributes attributes = new();
attributes.SetColorMatrix(new ColorMatrix { Matrix33 = 0.5f }); // half the alpha
attributes.SetWrapMode(WrapMode.Tile);
g.DrawImage(source, new Rectangle(20, 0, 40, 40), 0, 0, 16, 16, GraphicsUnit.Pixel, attributes);
Fonts
FontFamily finds a family among the installed fonts, scanned without native calls (on Linux the fontconfig folders and the
usual share/fonts folders), or in a PrivateFontCollection filled from files or memory; InstalledFontCollection and
FontFamily.Families list them. GetEmHeight, GetCellAscent, GetCellDescent and GetLineSpacing are GDI+'s: the units per
em, the OS/2 Windows ascent and descent, and the hhea ascender, descender and line gap. A Font converts its size at 96 dpi for
SizeInPoints, GetHeight() and Height (the line spacing rounded up), at a resolution for GetHeight(dpi) and in the page
units for GetHeight(Graphics); sizes of zero and the Display unit throw ArgumentException. As in System.Drawing, a family
that is not installed makes new FontFamily(name) throw and makes a Font fall back to the generic sans-serif family, with
Name the family used and OriginalFontName the one asked for (see Fonts that are not installed
for stand-ins). Missing bold and italic faces are synthesized. ToLogFont and FromLogFont read and write the
Interop.LOGFONT struct, with no GDI handle; SystemFonts gives Windows 10 and 11's interface fonts, GenericFontFamilies names
the serif, sans-serif and monospace families, and FontConverter converts fonts to and from strings such as "Arial, 10pt,
style=Bold", with FontConverter.FontNameConverter and FontConverter.FontUnitConverter for the name and unit properties.
// Fonts from the installed families, or from files an application brings along.
using PrivateFontCollection fonts = new();
fonts.AddFontFile(file);
using FontFamily family = new("DejaVu Sans", fonts);
using Font title = new(family, 12, FontStyle.Bold); // 12 points: 16 pixels per em at 96 dpi
float lineSpacing = title.GetHeight(); // ascender + descender + line gap, in pixels
int ascent = family.GetCellAscent(FontStyle.Regular); // design units, of GetEmHeight per em
// A family that is not installed falls back to the generic sans-serif family, as in System.Drawing: Name is the family
// used, OriginalFontName the one asked for.
using Font missing = new("No Such Font", 10);
string asked = missing.OriginalFontName; // "No Such Font"
// With Egami.Fonts.Substitutes referenced, metric-compatible fonts stand in under the requested name, keeping line breaks.
SystemDrawingOptions.FontSubstitutes = Egami.Fonts.Substitutes.SubstituteFonts.Find;
using Font helvetica = new("Helvetica", 10); // Liberation Sans where Helvetica is not installed
string standIn = helvetica.Name; // "Helvetica"
title.ToLogFont(out LOGFONT logFont); // lfHeight -16, lfWeight 700
Egami.Fonts.Font egami = title.ToEgamiFont(); // for Egami.Drawing's canvases
Text
DrawString, MeasureString, MeasureCharacterRanges and GraphicsPath.AddString share one layout, GDI+'s as measured on
Windows. With the default StringFormat the text starts 1/6 em in from the origin or the rectangle's side, advances are 3 %
wider than designed, and MeasureString adds the padding on both sides and 1/8 em to the height: one line measures the font's
ascent and descent, more lines one line spacing each. StringFormat.GenericTypographic drops the padding and the widening and
measures the glyphs alone. In a rectangle, lines break after spaces and hyphens (mid-word when a word is too long), and the last
line that fits takes the rest of its paragraph, cut by StringTrimming (at a character or word, with an ellipsis, or keeping a
path's start and end); of the StringFormatFlags, LineLimit draws only whole lines, NoClip draws past the rectangle,
NoWrap keeps one line per line break and MeasureTrailingSpaces counts trailing spaces. Alignment and LineAlignment
place the lines, DirectionRightToLeft aligns them from the right, DirectionVertical turns them a quarter clockwise, tab stops
default to every four ems, and HotkeyPrefix hides ampersands and underlines the marked letters. MeasureCharacterRanges
returns whole-pixel regions placed by the glyphs' hinted advances at the device size, as GDI+ places them.
TextRenderingHint.SingleBitPerPixel and SingleBitPerPixelGridFit draw aliased text, and AntiAlias and AntiAliasGridFit
grayscale text. ClearTypeGridFit and SystemDefault (ClearType, as on a Windows desktop) draw a solid brush's text with
Egami.Drawing's GDI+ ClearType preset: the font's hints for GDI+, grid-fitted advances, GDI+'s LCD filter and a gamma of
1 + TextContrast / 10; other brushes, rotated or vertical text and translucent pixels fall back to grayscale.
StringDigitSubstitute and StringUnit are kept and reported; digits are drawn as written.
using Bitmap bitmap = new(240, 80);
using Graphics g = Graphics.FromImage(bitmap);
g.Clear(Color.White);
g.TextRenderingHint = Egami.SystemDrawing.Text.TextRenderingHint.AntiAlias;
// From a point: the text starts 1/6 em right of it, the first baseline the font's ascent below it.
g.DrawString("Save && E&xit", font, Brushes.Black, 4, 4, new StringFormat { HotkeyPrefix = HotkeyPrefix.Show });
// In a rectangle: wrapped at words, aligned, the last line that fits trimmed with an ellipsis, clipped to the rectangle.
using StringFormat format = new() { Alignment = StringAlignment.Center, Trimming = StringTrimming.EllipsisWord };
RectangleF box = new(4, 24, 150, 34);
g.DrawString("The quick brown fox jumps over the lazy dog again and again", font, Brushes.Navy, box, format);
// Measuring: the default format pads 1/6 em on each side; GenericTypographic measures the glyphs alone.
SizeF size = g.MeasureString("Save", font);
SizeF tight = g.MeasureString("Save", font, PointF.Empty, StringFormat.GenericTypographic);
g.MeasureString("The quick brown fox", font, new SizeF(80, 40), format, out int fitted, out int lines);
// Where characters fall, in whole pixels.
format.SetMeasurableCharacterRanges([new CharacterRange(4, 5)]);
Region[] quick = g.MeasureCharacterRanges("The quick brown fox", font, box, format);
Fonts that are not installed
Linux and macOS have none of Windows' core fonts, so Arial, Times New Roman and the others fall back to whatever sans-serif font is
installed, and text runs longer or shorter than on Windows. Reference the Egami.Fonts.Substitutes package and register its finder
once at startup: its metric-compatible open fonts (Liberation Sans for Arial and Helvetica, Liberation Serif for Times New Roman,
Liberation Mono for Courier New, Carlito for Calibri, Caladea for Cambria) then stand in under the requested name, so Font.Name
reports it and lines break where they do on Windows. Installed fonts always come first. Egami.SystemDrawing itself ships no fonts;
SystemDrawingOptions.FontSubstitutes takes any finder of the same shape, from a family name and style to a typeface. The
Fonts sample above registers it.
Drawing on 32-bit ARGB bitmaps
new Bitmap(width, height) makes a 32-bit ARGB bitmap, and by default a Graphics draws on its pixels in place, blending with
Egami's rounding: many times faster than GDI+'s way, and within a level of GDI+ where colors blend. ArgbDrawing.Exact draws
through a premultiplied copy of the area each call touches instead, rounding exactly as GDI+ does; set it for one Graphics with
Graphics.ArgbDrawing, or for every Graphics made afterwards with SystemDrawingOptions.ArgbDrawing. SourceCopy and the
premultiplied, 24-bit, 16-bit and indexed formats always round as GDI+ does.
// Draw on a bitmap as with System.Drawing: GDI+'s defaults are aliased shapes and whole-pixel centers.
using Bitmap bitmap = new(80, 60);
using (Graphics g = Graphics.FromImage(bitmap))
{
g.Clear(Color.White);
g.SmoothingMode = SmoothingMode.AntiAlias;
g.PageUnit = GraphicsUnit.Millimeter; // 1 mm is 96 / 25.4 pixels at the bitmap's 96 dpi
using Pen pen = new(Color.Navy, 0.5f);
g.DrawEllipse(pen, 2, 2, 15, 10);
}
Color corner = bitmap.GetPixel(0, 0); // white
// Egami's own setting: 32-bit ARGB bitmaps draw in place by default; Exact rounds blends exactly as GDI+ does, more slowly.
using (Graphics exact = Graphics.FromImage(bitmap))
{
exact.ArgbDrawing = ArgbDrawing.Exact; // or SystemDrawingOptions.ArgbDrawing for every Graphics made afterwards
exact.FillRectangle(Brushes.Gold, 60, 40, 10, 10);
}
Metafiles
Metafile reads WMF, EMF and EMF+ (and Image.FromFile returns one for them) through Egami.Metafiles. It is an Image sized by
its frame as GDI+ sizes it: an EMF's frame in pixels at the resolution of the device it was made for, one pixel more for the
included edge, and a placeable WMF's box at 96 pixels to the inch; GetBounds gives the frame unrounded and
PhysicalDimension in hundredths of a millimeter. GetMetafileHeader reports the MetafileType, version, bounds,
resolutions, size and EMF+ header, and a MetaHeader and WmfPlaceableFileHeader for WMF. DrawImage plays the metafile as
vectors into the destination. EnumerateMetafile calls an EnumerateMetafileProc back for each record as GDI+ enumerates them, with its
EmfPlusRecordType, flags and pinned data: a placeable WMF's records without header and end, an EMF's from header to end, and
for EMF+ the EMF header, the EMF+ records and the end record. Egami plays metafiles whole, so when the callback calls
PlayRecord, the metafile is drawn once the walk ends; the PlayRecordCallback GDI+ passes for that is always null. As in GDI+, Graphics.FromImage on a metafile throws
ExternalException and RotateFlip throws NotImplementedException; saving or copying it into a Bitmap rasterizes it.
// A metafile is an Image sized by its frame; DrawImage plays it as vectors, EnumerateMetafile walks its records.
using Metafile metafile = new(System.IO.Path.Combine(Images, "shapes-plus.emf"));
MetafileHeader header = metafile.GetMetafileHeader(); // EmfPlusDual, its bounds and resolution
int records = 0;
using Bitmap bitmap = new(400, 300);
using (Graphics g = Graphics.FromImage(bitmap))
{
g.Clear(Color.White);
g.DrawImage(metafile, new Rectangle(0, 0, 400, 300));
g.EnumerateMetafile(metafile, new PointF(0, 0), (type, flags, size, data, play) =>
{
records++;
return true;
});
}
Egami reads metafiles but does not record them: the constructors that would record one take an EmfType or a
MetafileFrameUnit, and throw NotSupportedException with the EGSD0004 warning.
Effects
The GDI+ 1.1 effects of Egami.SystemDrawing.Imaging.Effects apply to a bitmap with Bitmap.ApplyEffect, to an area or the
whole of it, or while drawing with DrawImage(image, effect). ColorMatrixEffect, ColorLookupTableEffect,
GrayScaleEffect, InvertEffect and SepiaEffect give GDI+'s pixels, and BrightnessContrastEffect, ColorBalanceEffect,
LevelsEffect, ExposureCurveEffect, DensityCurveEffect, MidtoneCurveEffect, WhiteSaturationCurveEffect and
BlackSaturationCurveEffect follow GDI+'s curves, measured on a gray ramp, to within a level. HighlightCurveEffect,
ShadowCurveEffect and ContrastCurveEffect (all ColorCurveEffect with a CurveChannel), TintEffect, VividEffect,
BlurEffect and SharpenEffect are Egami's own versions of the same adjustments.
// GDI+ 1.1's effects, computed with Egami's operations; the color effects match GDI+ to a level or so.
using Bitmap photo = new(System.IO.Path.Combine(Images, "duck.png"));
using (BrightnessContrastEffect brighter = new(30, 20))
{
photo.ApplyEffect(brighter, Rectangle.Empty);
}
using (GrayScaleEffect gray = new())
{
photo.ApplyEffect(gray, new Rectangle(0, 0, photo.Width / 2, photo.Height));
}
using BlurEffect blur = new(4, false);
using Bitmap target = new(photo.Width, photo.Height);
using Graphics g = Graphics.FromImage(target);
g.DrawImage(photo, blur);
Buffers, cached bitmaps and animation
BufferedGraphicsManager.Current and BufferedGraphicsContext allocate a BufferedGraphics: an off-screen bitmap whose
Graphics draws in the target's coordinates, copied onto the target by Render as GDI copies it, premultiplied colors made
opaque. A CachedBitmap is drawn at device pixels with DrawCachedBitmap. ImageAnimator steps animated GIFs through their
frames at their delays: Animate calls a handler when the next frame is due, and UpdateFrames selects it. Graphics is an
IDeviceContext, whose members throw. ImageConverter, IconConverter and ImageFormatConverter convert for designers and
serializers, ToolboxBitmapAttribute, BitmapSuffixInSameAssemblyAttribute, BitmapSuffixInSatelliteAssemblyAttribute and
Design.CategoryNameCollection serve the Windows Forms designer, and ContentAlignment and ColorMode complete the enums.
// Off-screen buffers, cached bitmaps and animated GIFs, as WinForms code uses them.
using Bitmap window = new(100, 80);
using Graphics screen = Graphics.FromImage(window);
using (BufferedGraphics buffer = BufferedGraphicsManager.Current.Allocate(screen, new Rectangle(10, 10, 80, 60)))
{
buffer.Graphics.Clear(Color.SteelBlue);
buffer.Render();
}
using Bitmap sprite = new(8, 8);
using CachedBitmap cached = new(sprite, screen);
screen.DrawCachedBitmap(cached, 2, 2);
using Image animation = Image.FromFile(System.IO.Path.Combine(Images, "loop.gif"));
bool animates = ImageAnimator.CanAnimate(animation); // true: more than one timed frame
ImageAnimator.UpdateFrames(animation);
Moving on to Egami
Bitmap.AsRaster gives the bitmap's pixels as an Egami raster: a view of the same memory, no copy, for 32-bit ARGB and
premultiplied ARGB, 24-bit RGB and the 16-bit formats, and an 8-bit RGBA copy for the others. Bitmap.FromRaster copies a raster
into a bitmap of the matching format. Graphics.Canvas is an Egami canvas over a 32-bit bitmap, set to the graphics' transform
and clip, so code can move to Egami's API one call at a time. Migrating from System.Drawing to Egami
walks through the move step by step.
// Move to Egami's own API one call at a time: a bitmap's pixels as a raster (no copy for 32-bit ARGB), the graphics' canvas.
using Bitmap bitmap = new(64, 48);
Raster view = bitmap.AsRaster(); // Raster<Bgra8> over the bitmap's own pixels
using (Graphics g = Graphics.FromImage(bitmap))
{
g.TranslateTransform(10, 10);
Egami.Drawing.Canvas canvas = g.Canvas; // drawn in the graphics' world coordinates
canvas.FillCircle(new System.Numerics.Vector2(5, 5), 4, new Egami.Pixels.Rgba8(255, 0, 0, 255));
}
using Bitmap back = Bitmap.FromRaster(view); // a copy, 32-bit ARGB
What throws on every OS
Members that need Windows handles exist so code compiles, throw PlatformNotSupportedException with a message naming what to
use instead, and carry [Obsolete] with one diagnostic id per group, so the compiler warns at each use. Suppress an id in the
project file (<NoWarn>$(NoWarn);EGSD0001</NoWarn>), around a call (#pragma warning disable EGSD0001), or raise it to an
error in .editorconfig.
- EGSD0001, Windows handles:
Bitmap.GetHbitmap,GetHicon,FromHiconandFromResource,Image.FromHbitmap,Icon.HandleandIcon.FromHandle,Graphics.FromHdc,FromHwnd,GetHdc,ReleaseHdcandGetHalftonePalette, andRegion.FromHrgnandGetHrgn,Font.FromHdc,FromHfont,ToHfontand the device-context overloads ofFromLogFont, theMetafilehandle constructors andGetHenhmetafile, and the device-context overloads ofBufferedGraphicsContext.AllocateandBufferedGraphics.Render. UseLockBitsfor the pixels,new Icon(path)for icons andGraphics.FromImageto draw.
- EGSD0002, screen capture:
Graphics.CopyFromScreen, with or without aCopyPixelOperation.
- EGSD0003, Windows' own icons:
Icon.ExtractAssociatedIconandIcon.ExtractIcon, andSystemIconswith itsGetStockIconand theStockIconIdandStockIconOptionsit takes. Load an icon file instead.
- EGSD0004, metafile recording: the
Metafileconstructors that record a new metafile. Draw on aBitmap, or write SVG or PDF with Egami.Drawing's canvases.
Known differences
The package is measured against System.Drawing on Windows with the same probe programs compiled against both (egami-compare,
sysdrawing/). Pixel formats, conversions, palettes, strides, locks and the codec list match GDI+ exactly. Where it differs:
- JPEG and GIF files Egami writes differ from GDI+'s in their pixels: the JPEG encoder and GIF color mapping are Egami's.
CreateOptimalPalettechooses different colors.- CMYK JPEG and TIFF files load as
Format24bppRgb; GDI+ gives them a CMYK format System.Drawing has no name for. - Egami reads BMP files GDI+ rejects (OS/2, 2-bit, RLE24, embedded PNG and JPEG).
- Antialiased edges are Egami's: about 96 percent of the pixels of an antialiased shape match GDI+ within 8 levels, and aliased shapes match on about 99.8 percent of their pixels.
HighQualityBilinearandHighQualityBicubicsample as plain bilinear and bicubic, without GDI+'s wider filters, and an image drawn much smaller is not prefiltered.- On 32-bit ARGB bitmaps, translucent colors and antialiased edges blend with Egami's rounding, within a level of GDI+'s, unless
ArgbDrawing.Exactis set (Drawing on 32-bit ARGB bitmaps). - Without Egami.Fonts.Substitutes registered, a Windows font that is not installed falls back to the installed sans-serif font, so text measures differently from Windows (Fonts that are not installed).
- Drawing onto 16-bit bitmaps does not dither, and
SourceCopywith antialiasing blends its edges with what is below instead of making them transparent. - Compound pens draw their strips on both sides of the line, and
PathGradientBrush.FocusScalesis kept but not drawn. - A WMF without a placeable header is read as WMF, sized by its first window extent; GDI+ converts it to EMF first, so its size, header and records differ. Metafiles are drawn by Egami.Metafiles, which is close to GDI+ but not equal to it.
BlurEffect,SharpenEffect,TintEffect,VividEffectand the highlight, shadow and contrast curves are Egami's own versions;PlayRecordplays the whole metafile, not one record.- Text is laid out as GDI+ lays it out (line breaks, trimming and alignment match), and
MeasureStringagrees with GDI+ to about a hundredth of a pixel for Arial, Times New Roman, Courier New, Segoe UI and Microsoft Sans Serif. Glyphs are Egami's outlines: about 96 percent of the pixels of ClearType text and 90 percent of grayscale text match GDI+, and grayscale and aliased glyphs are not hinted. Characters a font lacks are not taken from fallback fonts, and digits are not substituted. Flattentakes as many steps as GDI+ but evaluates the curve exactly, so its points differ from GDI+'s, which are rounded to sixteenths of a pixel. Rounded rectangles have no zero-length segments, which GDI+'s float arithmetic adds.