Skip to content

YodasMyDad/ImageResize

Repository files navigation

ImageResize

A minimal, cross-platform image resize middleware for .NET Core that provides a drop-in replacement for some common ImageSharp functionality. Built with SkiaSharp for fast, reliable image processing across Windows, Linux, and macOS.

NuGet License: MIT .NET

Projects

ImageResize.Core (NuGet Package)

ASP.NET Core middleware for on-the-fly image resizing with disk caching.

ImageResize.ContextMenu (Windows Application)

Windows 11 context menu integration for quick image resizing. Right-click any image file and select "Resize Images..." to quickly resize single or multiple images. See ImageResize.ContextMenu/README.md for installation instructions.

Features

  • Querystring-based resizing: ?width=800&height=600&quality=80
  • Aspect ratio preservation: Always fits within specified dimensions
  • Multiple formats: JPEG, PNG, WebP, GIF (first frame), BMP, TIFF (first page)
  • Disk caching: Atomic writes with configurable sharding and size management
  • HTTP caching: ETags, Last-Modified, Cache-Control headers
  • Concurrency safe: Prevents thundering herd with keyed locks
  • Security: Path traversal protection and bounds validation
  • Backend support: Extensible codec architecture (SkiaSharp, future backends)
  • ImageSharp Compatibility Layer: Drop-in replacement for common ImageSharp operations
  • OSS-friendly: MIT licensed with no commercial restrictions

Installation

dotnet add package ImageResize

Quick Start

Program.cs

using ImageResize.Core.Extensions;

var builder = WebApplication.CreateBuilder(args);

// Simple setup with automatic defaults
builder.Services.AddImageResize(builder.Environment);

var app = builder.Build();

app.UseImageResize(); // Before UseStaticFiles and before routing app.UseRouting() etc...
app.UseStaticFiles();

app.Run();

Advanced Configuration (Optional)

builder.Services.AddImageResize(o =>
{
    o.ContentRoots = ["img", "images", "media"];  // URL path prefixes to monitor
    o.WebRoot = builder.Environment.WebRootPath;  // Where original images are stored (wwwroot)
    o.CacheRoot = Path.Combine(builder.Environment.WebRootPath, "_imgcache"); // Where resized images are cached
    o.AllowUpscale = false;                       // Don't enlarge images beyond original size
    o.DefaultQuality = 85;                        // Default JPEG/WebP quality
    o.PngCompressionLevel = 6;                    // PNG compression (0-9)
    o.Backend = ImageBackend.SkiaSharp;           // Image processing backend
    o.Cache.MaxCacheBytes = 1073741824;           // 1GB cache limit (0 = unlimited)
    o.Cache.PruneOnStartup = true;                // Clean old cache files on app start
});

URL Structure and File System Mapping

The middleware intercepts requests that start with any of the configured ContentRoots (default: ["img", "images", "media"]) and serves images from their actual paths. Here's how it works:

URL Pattern:

/{content-root-path}/{image-path}?width={width}&height={height}&quality={quality}

File System Mapping:

  • Original Images: Served from their actual location in WebRoot (default: wwwroot/)
  • Cached Resized Images: Stored in CacheRoot (default: wwwroot/_imgcache/)

Examples:

  • Request: GET /img/photos/cat.jpg?width=800

    • Original file: wwwroot/img/photos/cat.jpg
    • Cached file: wwwroot/_imgcache/{hash}/cat.jpg
  • Request: GET /images/banner.png?width=1920&height=600

    • Original file: wwwroot/images/banner.png
    • Cached file: wwwroot/_imgcache/{hash}/banner.png
  • Request: GET /media/inner/photo.jpg?width=300

    • Original file: wwwroot/media/inner/photo.jpg
    • Cached file: wwwroot/_imgcache/{hash}/photo.jpg
  • Request: GET /assets/logo.png?width=200Ignored (not in ContentRoots)

Key Points:

  • Images are served from their actual paths - no URL masking or rewriting
  • Only paths starting with configured ContentRoots are processed for resizing
  • Original images remain untouched in their original location
  • Resized images are automatically cached in a separate folder for performance
  • Cache uses SHA1 hashing with folder sharding to prevent filesystem issues with many files

Usage Examples

# Resize to 800px width (preserves aspect ratio)
GET /images/photos/cat.jpg?width=800

# Fit within 800x600 box
GET /img/photos/cat.jpg?width=800&height=600

# Resize with quality control
GET /media/photos/cat.jpg?height=1080&quality=85

# Serve original image (no resize parameters)
GET /images/photos/cat.jpg

# Different content roots work independently
GET /img/banner.jpg?width=1920
GET /images/logo.png?width=200
GET /media/video-thumbnail.jpg?width=640

# Paths not in ContentRoots are ignored
GET /assets/icon.png?width=128  # Ignored, served by static files middleware

Configuration

appsettings.json

{
  "ImageResize": {
    "EnableMiddleware": true,              // Enable/disable the middleware
    "ContentRoots": ["img", "images", "media"], // URL path prefixes to monitor
    "WebRoot": "wwwroot",                  // Root directory containing original images
    "CacheRoot": "wwwroot/_imgcache",      // Directory for cached resized images
    "AllowUpscale": false,                 // Prevent enlarging images beyond original size
    "DefaultQuality": 99,                  // Default JPEG/WebP quality (1-100)
    "PngCompressionLevel": 6,              // PNG compression level (0-9)
    "Bounds": {
      "MinWidth": 16, "MaxWidth": 4096,    // Width limits in pixels
      "MinHeight": 16, "MaxHeight": 4096,  // Height limits in pixels
      "MinQuality": 10, "MaxQuality": 100  // Quality limits (JPEG/WebP only)
    },
    "HashOriginalContent": false,          // Include file content in cache key (slower but more accurate)
    "Cache": {
      "FolderSharding": 2,                 // Subfolder levels for cache organization (0-4)
      "PruneOnStartup": false,             // Clean old cache files when app starts
      "MaxCacheBytes": 0                   // Cache size limit in bytes (0 = unlimited)
    },
    "ResponseCache": {
      "ClientCacheSeconds": 604800,        // Browser cache duration (7 days)
      "SendETag": true,                    // Send ETag headers for caching
      "SendLastModified": true             // Send Last-Modified headers for caching
    },
    "AllowedExtensions": [".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".tif", ".tiff"],
    "Backend": "SkiaSharp"                 // Image processing backend
  }
}

Programmatic Usage

// Inject the service
public class MyController : ControllerBase
{
    private readonly IImageResizerService _resizer;

    public MyController(IImageResizerService resizer)
    {
        _resizer = resizer;
    }

    [HttpGet("thumbnail/{id}")]
    public async Task<IActionResult> GetThumbnail(int id)
    {
        var result = await _resizer.EnsureResizedAsync(
            $"photos/{id}.jpg",
            new ResizeOptions(Width: 300, Height: 300, Quality: 80)
        );

        return PhysicalFile(result.CachedPath, result.ContentType);
    }
}

Enhanced ImageResult Properties

The ImageResult class provides all the properties you need:

using var image = await stream.LoadAsync(resizerService);

// Basic properties (ImageSharp compatible)
int width = image.Width;
int height = image.Height;
string contentType = image.ContentType;

// Enhanced metadata
long fileSize = image.FileSize;                    // File size in bytes
string fileSizeHuman = image.FileSizeHumanReadable; // "1.2 MB"
string format = image.Format;                      // "JPEG", "PNG", etc.
string extension = image.FileExtension;            // ".jpg", ".png", etc.

// Original dimensions
int originalWidth = image.OriginalWidth;
int originalHeight = image.OriginalHeight;

// Processing information
bool wasResized = image.WasResized;
bool isProcessed = image.IsProcessed;
int? quality = image.Quality;

// Computed properties
double aspectRatio = image.AspectRatio;            // Width/Height ratio
bool isLandscape = image.IsLandscape;
bool isPortrait = image.IsPortrait;
bool isSquare = image.IsSquare;
long pixelCount = image.PixelCount;               // Total pixels

Usage Examples

First, get a stream from your image source:

// From a file path
using var fileStream = File.OpenRead("path/to/image.jpg");

// From an HTTP file upload (ASP.NET Core)
public async Task<IActionResult> UploadImage(IFormFile uploadedFile)
{
    await using var stream = uploadedFile.OpenReadStream();
    // ... process stream
}

// From a byte array
var imageBytes = await File.ReadAllBytesAsync("path/to/image.jpg");
using var memoryStream = new MemoryStream(imageBytes);

// From a URL
using var httpClient = new HttpClient();
using var response = await httpClient.GetAsync("https://example.com/image.jpg");
using var urlStream = await response.Content.ReadAsStreamAsync();

Then process the stream:

// Simple resize with one method call
var options = new ResizeOptions(Width: 1920, Height: 1080, Quality: 90);
using var resizedImage = await resizerService.ResizeAsync(stream, null, options);

// Save the result
await resizedImage.SaveAsync(filePath);

Convenience Methods

For common resize operations, use these simple extension methods:

// Resize to specific width (maintains aspect ratio)
using var resized = await resizerService.ResizeToWidthAsync(stream, 800);

// Resize to specific height (maintains aspect ratio)
using var resized = await resizerService.ResizeToHeightAsync(stream, 600);

// Resize to fit within dimensions (maintains aspect ratio)
using var resized = await resizerService.ResizeToFitAsync(stream, 1920, 1080);

Cache Design

  • Key generation: SHA1 hash of normalized path + options + source signature
  • Source signature: Last modified time + file size (+ optional content hash)
  • Atomic writes: Temp file → rename for consistency
  • Folder sharding: Configurable subfolder splitting (e.g., ab/cd/hash.ext)
  • Size management: Automatic cleanup when MaxCacheBytes exceeded
  • Startup pruning: Optional cleanup of old files on application startup

Supported Formats

Format Read Write Notes
JPEG Quality 1-100
PNG Compression level 0-9 (configurable)
WebP Quality 1-100
GIF First frame only
BMP
TIFF First page only

Backend Support

Currently supports SkiaSharp backend with framework for additional backends:

  • SkiaSharp: Cross-platform, high-performance (default)
  • SystemDrawing: Windows-only, .NET Framework compatible (planned)
  • MagickNet: ImageMagick integration (planned)

Configure via Backend setting in appsettings.json.

Performance

  • Memory efficient: Streams data without loading entire images
  • Concurrent safe: Keyed locks prevent duplicate processing
  • HTTP optimized: Conditional requests (304) and client caching
  • Configurable quality: Balance file size vs. visual quality
  • Smart caching: Automatic cache size management and startup pruning
  • Backend flexibility: Choose optimal codec for your platform

Security

  • Path traversal prevention
  • Configurable size bounds
  • Input validation on all parameters
  • Safe file operations with atomic writes

License

MIT

Contributing

PRs welcome! See the Example app for usage examples and tests for implementation details.

About

Minimal, cross-platform image resize middleware for .NET Core

Resources

License

Stars

Watchers

Forks