Migrating from System.Drawing to Egami
Egami.SystemDrawing runs System.Drawing code unchanged; this guide moves that code to Egami's own API one piece at a time, with
the four escape hatches that join the two: Bitmap.AsRaster, Bitmap.FromRaster, Graphics.Canvas and Font.ToEgamiFont. At
every step the program builds, runs and gives the same pictures, so the move can stop anywhere.
Step 0: the drop-in
Replace the System.Drawing.Common package reference with Egami.SystemDrawing, as the drop-in guide
describes: System.Drawing and Egami.SystemDrawing come in as implicit usings, and files that name System.Drawing.Imaging,
System.Drawing.Drawing2D or System.Drawing.Text change those lines to Egami.SystemDrawing.Imaging, .Drawing2D and
.Text. Members that need Windows handles compile with a warning, EGSD0001 to EGSD0004, and throw when called: each warning
marks code that has to change anyway (an HBITMAP becomes LockBits or AsRaster, CopyFromScreen has no replacement, an
Icon comes from a file). The drop-in guide's list names every member.
The strategy
Keep Bitmap, Image and Graphics at the edges of the program, where they meet other code and public APIs, and move the hot
spots first: pixel loops, resizing, filters, batch encoding. An escape hatch turns a bitmap into an Egami raster for one part of a
routine and back for the rest, so each change is small and tested on its own. When no Bitmap crosses a boundary any more, drop
the package.
Pixels: Bitmap.AsRaster
AsRaster gives a bitmap's pixels as a Raster<TPixel> without copying them for 32-bit ARGB (Raster<Bgra8>), premultiplied
ARGB (Raster<PremulBgra8>), 24-bit RGB (Raster<Bgr8>) and the 16-bit formats; other formats give an 8-bit RGBA copy. Egami's
operations (Apply changes a raster, Derive makes a new one) and its row spans then work on the bitmap itself, in place of
LockBits loops, ImageAttributes color matrices and hand-written filters.
The view stays valid as long as you hold it, but RotateFlip, MakeTransparent, a format change (ConvertFormat) and
SelectActiveFrame give the bitmap new pixels, which an old view no longer shows: fetch the view again after them.
// A 32-bit or 24-bit bitmap's pixels as an Egami raster, no copy: Egami's operations change the bitmap itself.
using (Bitmap argb = bitmap.Clone(new Rectangle(0, 0, bitmap.Width, bitmap.Height), PixelFormat.Format32bppArgb))
{
var view = (Raster<Bgra8>)argb.AsRaster();
view.Apply(ops => ops.Grayscale().Sharpen(0.5f));
Color gray = argb.GetPixel(10, 10); // R, G and B are equal now
// RotateFlip, a format change or SelectActiveFrame give the bitmap new pixels: fetch the view again after them.
argb.RotateFlip(RotateFlipType.Rotate90FlipNone);
view = (Raster<Bgra8>)argb.AsRaster();
Assert.Equal(gray.R, gray.B);
Assert.Equal(argb.Width, view.Width);
}
Results back: Bitmap.FromRaster
Bitmap.FromRaster copies any Egami raster into a bitmap of the matching format (Bgra8 into 32-bit ARGB, Bgr8 into 24-bit
RGB, other pixel types through 8-bit RGBA), for code that still takes a Bitmap or Image.
// An Egami result handed back to code that still takes a Bitmap: FromRaster copies it into the matching pixel format.
using Raster<Rgba8> badge = Raster.Create(64, 64, new Rgba8(30, 110, 200, 255));
badge.Apply(ops => ops.Vignette());
using Bitmap forOldCode = Bitmap.FromRaster(badge); // 32-bit ARGB
forOldCode.Save(System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"badge-{Guid.NewGuid():N}.png"), ImageFormat.Png);
Drawing: Graphics.Canvas
Graphics.Canvas is an Egami.Drawing canvas over a 32-bit bitmap, set to the graphics' world transform and clip, so Egami
paths, paints, gradients, images and text can be drawn between Graphics calls. A routine moves call by call; once it only uses
the canvas, draw on raster.CreateCanvas() instead.
// Egami.Drawing calls in the middle of an existing Graphics: the canvas draws in the graphics' world coordinates and clip.
using Bitmap bitmap = new(120, 80);
using (Graphics g = Graphics.FromImage(bitmap))
{
g.Clear(Color.White);
g.TranslateTransform(10, 10);
g.DrawRectangle(Pens.Black, 0, 0, 60, 40); // System.Drawing, as before
Egami.Drawing.Canvas canvas = g.Canvas; // Egami, at the same place
canvas.FillPath(Egami.Drawing.Path.Star(new Vector2(30, 20), 18, 8, 5), new SolidPaint(new Rgba8(240, 160, 0, 255)) { Antialias = true });
}
Text: Font.ToEgamiFont
Font.ToEgamiFont gives the same family, style and size as an Egami.Fonts font, sized in pixels at 96 dpi by default, with
synthetic bold or italic where the family lacks the face. Egami shapes text (kerning, ligatures, Arabic, Indic and the other
complex scripts), lays out paragraphs with TextLayout and draws with Canvas.DrawText; Font.MeasureText replaces
MeasureString.
// The same font in Egami.Fonts: shaped text (kerning, ligatures, complex scripts), measured and drawn by Egami.Drawing.
EgamiFonts.Font font = caption.ToEgamiFont(); // 16 pixels per em at 96 dpi, synthetic bold or italic as needed
using Raster<Rgba8> label = Raster.Create(200, 30, new Rgba8(255, 255, 255, 255));
using (RasterCanvas<Rgba8> canvas = label.CreateCanvas())
{
canvas.DrawText("Shaped by Egami", new Vector2(4, 20), font, new Rgba8(0, 0, 0, 255));
}
float width = font.MeasureText("Shaped by Egami").Advance;
Files: Raster.Load and Save
Image.FromFile and Image.FromStream become Raster.Load, which reads every format Egami knows; Raster.Probe reads the
size, format and frames from the header alone. Save takes the format's own writer options (JpegWriterOptions,
PngWriterOptions, WebpWriterOptions and the others) instead of ImageCodecInfo and EncoderParameters, and metadata stays
in the raster's Metadata instead of PropertyItems. The formats overview and each format's page cover the options.
// Image.FromFile and Save become Raster.Load and Save; Probe reads the header alone, and each format has its own options.
Egami.Formats.RasterInfo info = Raster.Probe(input); // size, format, frames, without decoding pixels
using Raster photo = Raster.Load(input);
photo.Save(output, new JpegWriterOptions { Quality = 85 }); // instead of EncoderParameters
What maps to what
| System.Drawing | Egami |
|---|---|
Bitmap, Image |
Raster<TPixel>, such as Raster<Rgba8>; Raster when the pixel type does not matter |
PixelFormat |
the pixel type: Rgba8, Bgra8, PremulBgra8, Rgb8, Gray8, Rgba16 and the others |
GetPixel, SetPixel, LockBits |
Raster<TPixel>.GetPixel, SetPixel and Row(y) spans |
Image.FromFile, FromStream, Save, EncoderParameters |
Raster.Load, Raster.Probe, Save with the format's writer options |
PropertyItem, GetPropertyItem |
Raster.Metadata (EXIF, XMP, IPTC, ICC) |
RotateFlip, GetThumbnailImage, Clone(rect) |
operations: Rotate, FlipHorizontal, FlipVertical, Fit, Resize, Crop |
ImageAttributes, ColorMatrix, SetGamma |
operations: ColorMatrix, Gamma, Grayscale, Brightness and the rest |
Graphics, Graphics.FromImage |
Canvas, from raster.CreateCanvas() |
Pen, DashStyle, LineCap, LineJoin |
Stroke (width, dashes, caps, joins) with a Paint |
SolidBrush, LinearGradientBrush, PathGradientBrush, TextureBrush |
SolidPaint, LinearGradient, RadialGradient, ImagePaint |
GraphicsPath, Region |
Path, PathBuilder, Path.Union and the other path operations |
Matrix, TranslateTransform, RotateTransform |
System.Numerics.Matrix3x2, Canvas.Concat, Translate, Rotate, Scale |
SetClip, IntersectClip |
Canvas.ClipRect, ClipPath |
Font, FontFamily, InstalledFontCollection |
Egami.Fonts.Font, Typeface, FontCollection.System |
DrawString, MeasureString, StringFormat |
Canvas.DrawText, Font.MeasureText, TextLayout with a ParagraphStyle |
Metafile |
Egami.Metafiles.Metafile |
Icon |
ICO and CUR files through Raster.Load and Save |
The last step
When no Bitmap, Image, Graphics or Font crosses a boundary of your code any more, remove the Egami.SystemDrawing
package reference and its implicit usings. What is left uses Egami, Egami.Drawing and Egami.Fonts directly; the compiler lists
anything that still names a drop-in type.
Recipes
Move a thumbnail with a watermark in three steps
A routine that scales a photo to 200 pixels wide, writes a caption on it and saves a JPEG, as System.Drawing code writes it. It runs unchanged on the drop-in:
// Step 1: the System.Drawing code as it was, compiled against Egami.SystemDrawing.
private static void Step1(string input, string output)
{
using Image source = Image.FromFile(input);
int height = source.Height * 200 / source.Width;
using Bitmap thumb = new(200, height);
using (Graphics g = Graphics.FromImage(thumb))
{
g.InterpolationMode = Egami.SystemDrawing.Drawing2D.InterpolationMode.HighQualityBicubic;
g.DrawImage(source, new Rectangle(0, 0, 200, height));
using Egami.SystemDrawing.Font font = new("Arial", 10);
g.DrawString("© Egami", font, Brushes.White, 4, height - 20);
}
thumb.Save(output, ImageFormat.Jpeg);
}
The scaling moves to Egami first: the photo loads as a raster, Resize makes the thumbnail, and Bitmap.FromRaster hands it
to the drawing and saving code, which does not change:
// Step 2: the hot spot (scaling) moves to Egami; the bitmap stays at the edges, so the watermark code is untouched.
private static void Step2(string input, string output)
{
using Raster loaded = Raster.Load(input);
using Raster<Rgba8> small = loaded.As<Rgba8>().Derive(ops => ops.Resize(200, 0));
using Bitmap thumb = Bitmap.FromRaster(small);
using (Graphics g = Graphics.FromImage(thumb))
{
using Egami.SystemDrawing.Font font = new("Arial", 10);
g.DrawString("© Egami", font, Brushes.White, 4, thumb.Height - 20);
}
thumb.Save(output, ImageFormat.Jpeg);
}
Then the caption and the save move too, and the routine uses Egami alone:
// Step 3: all Egami: load, resize, draw the watermark on the raster's canvas, save with the JPEG options.
private static void Step3(string input, string output)
{
using Raster loaded = Raster.Load(input);
using Raster<Rgba8> thumb = loaded.As<Rgba8>().Derive(ops => ops.Resize(200, 0));
EgamiFonts.Font font = new(EgamiFonts.Typeface.FromFile(FontFile), 13);
using (RasterCanvas<Rgba8> canvas = thumb.CreateCanvas())
{
canvas.DrawText("© Egami", new Vector2(6, thumb.Height - 8), font, new Rgba8(255, 255, 255, 255));
}
thumb.Save(output, new JpegWriterOptions { Quality = 85 });
}