.NET canvas library for loading, saving, and cropping images
CanvasNet is a .NET library providing a mutable, span-based 32-bit RGBA pixel buffer along with
codecs for loading and saving images in common file formats. It's designed for fast, allocation-conscious
image operations using Span<T>, and supports independent-copy cropping for load/crop/save workflows.
- 🖼️ Pixel Buffer - Mutable 32-bit RGBA surface with span access
- ✂️ Cropping - Independent-copy crop for load/crop/save workflows
- 🌈 Compositing - Alpha premultiply and Porter-Duff "over" compositing
- 📀 BMP Codec - Load/save 24-bit/32-bit uncompressed BMP files
- 🎨 PNG Codec - Load non-interlaced PNGs; save 8-bit RGB/RGBA
- 🖨️ TIFF Codec - Load/save 8-bit RGB/RGBA/Grayscale TIFF files
- 🗜️ JPEG Codec - Load baseline/progressive; save baseline JPEG
- 🎞️ GIF Codec - Decode-only load of first GIF frame;
GetInforeports the true frame count - 📐 SVG Codec - Rasterize a common SVG subset, including markers/filters/clip-paths/masks and
weight/style-aware font matching, to a surface (ships as the separate
DemaConsulting.CanvasNet.Svgpackage) - 📄 PDF Document - Open a PDF, inspect its page count/size/rotation, and rasterize a page's
path geometry, device color (including shading and tiling pattern fills), image XObjects, and
TrueType text to a surface, automatically substituting a matching system font (or a bundled
Liberation Sans/Serif/Mono font) for text using a non-embedded font (ships as the separate
DemaConsulting.CanvasNet.Pdfpackage) - 🔍 Header-Only Probing -
GetInforeads headers without decoding pixels (GIF excepted) - 🖌️ Path Filling - Antialiased nonzero/even-odd fill of vector paths
- 🖊️ Stroke-to-Fill - Convert stroked paths into fillable outlines
- 🌅 Gradient Paint - Linear or radial gradient fills with spread
- 🧱 Tile Paint - Fill a path by repeating a pre-rendered tile bitmap at a configurable pattern-space pitch and transform
- 🔤 TrueType/CFF Fonts - Load TrueType (
glyf) or CFF/OpenType (.otf) fonts and individual faces of a TrueType Collection (.ttc), map codepoints, extract glyph outlines, and query name/style metadata (family/subfamily/full/PostScript name, bold/italic/fixed-pitch) - 🗂️ System Font Discovery -
Fonts.SystemFontCatalogdiscovers fonts installed on the host operating system, best-effort matches a requested family/style against them, and provides a bundled Liberation Sans/Serif/Mono last-resort fallback font - 🎬 Rendering - Transform-aware canvas with text and shape drawing
- ⚡ Span-Based - Fast, allocation-conscious pixel and row access
- 🔄 Multi-Target - Supports .NET 8, 9, and 10
- 📦 NuGet Ready - Easy integration via NuGet package
dotnet add package DemaConsulting.CanvasNetOr via Package Manager Console:
Install-Package DemaConsulting.CanvasNetSVG rasterization requires the separate DemaConsulting.CanvasNet.Svg package:
dotnet add package DemaConsulting.CanvasNet.SvgOr via Package Manager Console:
Install-Package DemaConsulting.CanvasNet.SvgPDF document parsing requires the separate DemaConsulting.CanvasNet.Pdf package:
dotnet add package DemaConsulting.CanvasNet.PdfOr via Package Manager Console:
Install-Package DemaConsulting.CanvasNet.Pdfusing DemaConsulting.CanvasNet.Canvas;
using DemaConsulting.CanvasNet.Codecs;
using DemaConsulting.CanvasNet.Svg;
using DemaConsulting.CanvasNet.Pdf;
using System.IO;
// Create a surface, set a pixel, and crop an independent copy
using var surface = new Surface(4, 4);
surface[1, 1] = new Rgba32(255, 0, 0, 255);
using var cropped = surface.Crop(0, 0, 2, 2);
// Save as BMP and load it back
BmpCodec.Save(surface, "surface.bmp");
using var reloaded = BmpCodec.Load("surface.bmp");
// Save as PNG and load it back
PngCodec.Save(surface, "surface.png");
using var reloadedPng = PngCodec.Load("surface.png");
// Save as TIFF and load it back
TiffCodec.Save(surface, "surface.tiff");
using var reloadedTiff = TiffCodec.Load("surface.tiff");
// Save as JPEG and load it back
JpegCodec.Save(surface, "surface.jpg", 90);
using var reloadedJpeg = JpegCodec.Load("surface.jpg");
// Decode-only: load the first frame of a GIF
using var reloadedGif = GifCodec.Load("surface.gif");
// GetInfo also reports a GIF's true total frame count; it decodes the first frame's
// LZW-compressed pixel data to validate CanDecode, but never resolves those pixels into a
// rendered Surface
var gifInfo = GifCodec.GetInfo("surface.gif");
Console.WriteLine($"Frames: {gifInfo.FrameCount}");
// Decode/rasterize an SVG into a 256x256 surface
using var rasterized = SvgCodec.Load("icon.svg", 256, 256);
// Open a PDF, inspect its (rotation-adjusted) page size, and render it. Render paints the
// page's real content-stream geometry: path fills/strokes with device color, placed image
// XObjects, and text (using the page's embedded font, or an automatically substituted
// system/bundled fallback font when none is embedded).
using var pdfDoc = PdfDocument.Open("document.pdf");
var pageInfo = pdfDoc.GetPageInfo(0);
using var pdfSurface = pdfDoc.Render(0, pageInfo.Width, pageInfo.Height);
// Triage an untrusted file's header before decoding pixel data
var info = PngCodec.GetInfo("untrusted.png");
if (info.Width > Surface.MaxDimension
|| info.Height > Surface.MaxDimension
|| (long)info.Width * info.Height > (long)Surface.MaxDimension * Surface.MaxDimension)
{
throw new InvalidDataException("Image dimensions exceed the supported maximum.");
}
// Reject files that declare a feature the codec cannot decode
if (!info.CanDecode)
{
throw new UnsupportedImageFeatureException(
"png-adam7-interlace",
"File is well-formed but declares an unsupported feature.");
}
// Now safe to decode fully
using var safeSurface = PngCodec.Load("untrusted.png");Filling a vector path onto a surface:
using DemaConsulting.CanvasNet.Canvas;
using DemaConsulting.CanvasNet.Drawing;
using DemaConsulting.CanvasNet.Geometry;
using System.Numerics;
using var canvas = new Surface(64, 64);
// Build a triangular path
var triangle = new PathBuilder()
.MoveTo(new Vector2(8, 56))
.LineTo(new Vector2(56, 56))
.LineTo(new Vector2(32, 8))
.Close()
.Build();
// Fill the triangle with an antialiased solid color
PathFiller.Fill(canvas, triangle, new Rgba32(0, 128, 255, 255));Stroking a vector path onto a surface:
using DemaConsulting.CanvasNet.Canvas;
using DemaConsulting.CanvasNet.Drawing;
using DemaConsulting.CanvasNet.Geometry;
using System.Numerics;
using var canvas = new Surface(64, 64);
// Build a zig-zag polyline
var polyline = new PathBuilder()
.MoveTo(new Vector2(8, 48))
.LineTo(new Vector2(32, 16))
.LineTo(new Vector2(56, 48))
.Build();
// Define a round-capped, round-joined, dashed stroke style
var style = new StrokeStyle(
width: 6f,
cap: LineCap.Round,
join: LineJoin.Round,
dashArray: [10f, 6f]);
// Convert the stroke to fillable outline geometry and fill it
var strokedOutline = PathStroker.Stroke(polyline, style);
PathFiller.Fill(canvas, strokedOutline, new Rgba32(255, 128, 0, 255));Filling a vector path with a linear gradient:
using DemaConsulting.CanvasNet.Canvas;
using DemaConsulting.CanvasNet.Drawing;
using DemaConsulting.CanvasNet.Geometry;
using System.Numerics;
using var canvas = new Surface(64, 64);
// Build a square path
var rectangle = new PathBuilder()
.MoveTo(new Vector2(4, 4))
.LineTo(new Vector2(60, 4))
.LineTo(new Vector2(60, 60))
.LineTo(new Vector2(4, 60))
.Close()
.Build();
// Define a red-to-blue horizontal gradient
var gradient = new LinearGradient(
start: new Vector2(4, 0),
end: new Vector2(60, 0),
stops:
[
new GradientStop(0f, new Rgba32(255, 0, 0, 255)),
new GradientStop(1f, new Rgba32(0, 0, 255, 255))
]);
// Fill the square with the gradient
PathFiller.Fill(canvas, rectangle, gradient, FillRule.NonZero, 1f);Loading a TrueType (or CFF/OpenType) font and filling a glyph outline:
using DemaConsulting.CanvasNet.Canvas;
using DemaConsulting.CanvasNet.Drawing;
using DemaConsulting.CanvasNet.Fonts;
using DemaConsulting.CanvasNet.Geometry;
using System.Numerics;
// Convert a glyph outline from font units (Y up) to canvas space (Y down).
// glyf-flavored fonts produce QuadraticBezierTo segments; CFF/OpenType (.otf)
// fonts produce CubicBezierTo segments - both are handled here.
static Path TransformGlyph(Path glyph, float scale, float baselineY)
{
var builder = new PathBuilder();
Vector2 ToCanvas(Vector2 point) => new(point.X * scale, baselineY - point.Y * scale);
foreach (var subpath in glyph.Subpaths)
{
builder.MoveTo(ToCanvas(subpath.Start));
foreach (var command in subpath.Commands)
{
switch (command.Type)
{
case PathCommandType.LineTo:
builder.LineTo(ToCanvas(command.EndPoint));
break;
case PathCommandType.QuadraticBezierTo:
builder.QuadraticBezierTo(
ToCanvas(command.Control1),
ToCanvas(command.EndPoint));
break;
case PathCommandType.CubicBezierTo:
builder.CubicBezierTo(
ToCanvas(command.Control1),
ToCanvas(command.Control2),
ToCanvas(command.EndPoint));
break;
case PathCommandType.Close:
builder.Close();
break;
}
}
}
return builder.Build();
}
// Load the font (accepts .ttf, .otf, or a specific face of a .ttc) and get
// glyph 'A' scaled to a 48px em size
var font = TrueTypeFont.Load("font.ttf");
var glyphIndex = font.GetGlyphIndex('A');
var glyphOutline = font.GetGlyphOutline(glyphIndex);
var scale = 48f / font.UnitsPerEm;
var canvasOutline = TransformGlyph(glyphOutline, scale, baselineY: 56f);
// Fill the transformed glyph outline
using var surface = new Surface(64, 64);
PathFiller.Fill(surface, canvasOutline, new Rgba32(20, 120, 255, 255));Selecting a face from a TrueType Collection (.ttc):
// Discover how many faces the collection contains
var faceCount = TrueTypeFont.GetFaceCount("collection.ttc"); // e.g. 2
// Load a specific face by index (face 0 is used when Load is called without
// an index, matching an ordinary single-face font's default behavior)
var boldFace = TrueTypeFont.Load("collection.ttc", faceIndex: 1);Querying a font's name and style metadata:
// Resolve the font's family/subfamily/full/PostScript name from its name table
var nameInfo = font.GetNameInfo();
Console.WriteLine($"{nameInfo.FamilyName} {nameInfo.SubfamilyName}"); // e.g. "Open Sans Regular"
// Derived bold/italic/fixed-pitch classification (from OS/2, head.macStyle, and post)
if (font.IsBold || font.IsItalic || font.IsFixedPitch)
{
Console.WriteLine("Bold: {0}, Italic: {1}, Fixed-pitch: {2}",
font.IsBold, font.IsItalic, font.IsFixedPitch);
}pwsh ./build.ps1Detailed API documentation for all public types and members is distributed in the api/ folder
of the NuGet package.
The CanvasNet User Guide is available on the CanvasNet releases page.
Contributions are welcome. See CONTRIBUTING.md for development setup, coding standards, and the pull request process.
Copyright (c) DEMA Consulting. Licensed under the MIT License. See LICENSE for details.
By contributing to this project, you agree that your contributions will be licensed under the MIT License.
The DemaConsulting.CanvasNet package bundles the Liberation Sans, Liberation Serif, and
Liberation Mono TrueType fonts (12 files total) as embedded resources, used as a last-resort
fallback font by PdfDocument's automatic font-substitution feature. These fonts are Copyright
(c) 2012 Red Hat, Inc., licensed under the SIL Open Font License, Version 1.1; see
src/DemaConsulting.CanvasNet/Fonts/BundledFonts/OFL.txt and
src/DemaConsulting.CanvasNet/Fonts/BundledFonts/README.md for the full license text and
sourcing/provenance details.
The DemaConsulting.CanvasNet package also bundles the Noto Sans, Noto Sans Math, and Noto Sans
Symbols 2 TrueType fonts (NotoSans-Regular.ttf, NotoSansMath-Regular.ttf,
NotoSansSymbols2-Regular.ttf) as embedded resources, used as the dedicated substitute font for
Symbol/ZapfDingbats text by PdfDocument's automatic font-substitution feature. These fonts
are part of the Noto Project, licensed under the SIL Open Font License, Version 1.1; see
src/DemaConsulting.CanvasNet/Fonts/BundledFonts/NotoFonts-OFL.txt and the "Noto Substitute
Fonts" section of src/DemaConsulting.CanvasNet/Fonts/BundledFonts/README.md for the full license
text and sourcing/provenance details.