Skip to content

Latest commit

 

History

33 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

CanvasNet

GitHub forks GitHub stars GitHub contributors License Build Quality Gate Security NuGet

.NET canvas library for loading, saving, and cropping images

Overview

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.

Features

  • 🖼️ 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; GetInfo reports 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.Svg package)
  • 📄 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.Pdf package)
  • 🔍 Header-Only Probing - GetInfo reads 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.SystemFontCatalog discovers 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

Installation

dotnet add package DemaConsulting.CanvasNet

Or via Package Manager Console:

Install-Package DemaConsulting.CanvasNet

SVG rasterization requires the separate DemaConsulting.CanvasNet.Svg package:

dotnet add package DemaConsulting.CanvasNet.Svg

Or via Package Manager Console:

Install-Package DemaConsulting.CanvasNet.Svg

PDF document parsing requires the separate DemaConsulting.CanvasNet.Pdf package:

dotnet add package DemaConsulting.CanvasNet.Pdf

Or via Package Manager Console:

Install-Package DemaConsulting.CanvasNet.Pdf

Usage

using 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);
}

Building

pwsh ./build.ps1

API Documentation

Detailed API documentation for all public types and members is distributed in the api/ folder of the NuGet package.

User Guide

The CanvasNet User Guide is available on the CanvasNet releases page.

Contributing

Contributions are welcome. See CONTRIBUTING.md for development setup, coding standards, and the pull request process.

License

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.

Support

About

Pure DotNet Canvas Library

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages