In today’s digital age, efficient image handling is crucial for web applications and APIs. One of the key aspects of image management is compression, which helps in reducing file sizes without significantly compromising on quality. This guide walks you through building a dynamic image compression API using Aspose.Imaging for .NET. By the end, you’ll have a functional ASP.NET Core Web API that accepts images and returns compressed output according to query parameters (format, quality, resizing, and more).

Aspose.Imaging is a powerful library for working with images in .NET. It supports many formats and provides robust manipulation features, including lossy (JPEG) and lossless (PNG) workflows. We’ll leverage it to build an efficient, scalable compression service.

What You’ll Build

  • Endpoint: POST /api/images/compress?format=jpeg&quality=75&maxWidth=1280&maxHeight=1280
  • Inputs: Multipart file (image), optional query parameters for format/quality/resize
  • Outputs: Compressed image stream with correct Content-Type and caching headers
  • Safety: Content-type validation, size limits, and guarded decode/encode

Prerequisites

  • .NET 8 (or .NET 6+)
  • ASP.NET Core Web API project
  • NuGet: Aspose.Imaging
  • Optional: license initialization in app startup (if you’re using a licensed build)

Project Structure (minimal)

/Controllers
  ImageController.cs
/Services
  ImageCompressionService.cs
/Models
  CompressionRequest.cs
Program.cs
appsettings.json

Complete Example (Service + Controller)

Replace the placeholder namespaces with your project’s namespace.

/Models/CompressionRequest.cs

namespace ImageApi.Models;

public sealed class CompressionRequest
{
    // "jpeg" or "png"
    public string Format { get; init; } = "jpeg";

    // 1..100 (applies to JPEG only; PNG is lossless)
    public int? Quality { get; init; } = 80;

    // Optional resize bounds; image is resized preserving aspect ratio if either is provided.
    public int? MaxWidth { get; init; }
    public int? MaxHeight { get; init; }

    // If true, strip metadata (EXIF, IPTC) where applicable to reduce size further.
    public bool StripMetadata { get; init; } = true;

    // Guardrails
    public void Validate()
    {
        var fmt = Format?.ToLowerInvariant();
        if (fmt is not "jpeg" and not "png")
            throw new ArgumentException("Unsupported format. Use 'jpeg' or 'png'.");

        if (Quality is { } q && (q < 1 || q > 100))
            throw new ArgumentException("Quality must be between 1 and 100.");

        if (MaxWidth is { } w && w <= 0) throw new ArgumentException("MaxWidth must be positive.");
        if (MaxHeight is { } h && h <= 0) throw new ArgumentException("MaxHeight must be positive.");
    }
}

/Services/ImageCompressionService.cs

using Aspose.Imaging;
using Aspose.Imaging.ImageOptions;
using ImageApi.Models;

namespace ImageApi.Services;

public interface IImageCompressionService
{
    Task<(MemoryStream output, string contentType, string fileExt)> CompressAsync(
        Stream input, CompressionRequest req, CancellationToken ct = default);
}

public sealed class ImageCompressionService : IImageCompressionService
{
    private readonly ILogger<ImageCompressionService> _logger;

    public ImageCompressionService(ILogger<ImageCompressionService> logger)
    {
        _logger = logger;
    }

    public async Task<(MemoryStream output, string contentType, string fileExt)> CompressAsync(
        Stream input, CompressionRequest req, CancellationToken ct = default)
    {
        req.Validate();

        // Defensive copy to a seekable stream
        var inbound = new MemoryStream();
        await input.CopyToAsync(inbound, ct).ConfigureAwait(false);
        inbound.Position = 0;

        // Load image via Aspose.Imaging
        using var image = Image.Load(inbound);

        // Optional: strip metadata (where applicable)
        if (req.StripMetadata)
        {
            TryStripMetadata(image);
        }

        // Optional resize (preserve aspect ratio)
        if (req.MaxWidth.HasValue || req.MaxHeight.HasValue)
        {
            ResizeInPlace(image, req.MaxWidth, req.MaxHeight);
        }

        // Choose encoder and options
        string fmt = req.Format.ToLowerInvariant();
        var (options, contentType, ext) = BuildOptions(fmt, req.Quality);

        // Save to output
        var output = new MemoryStream();
        image.Save(output, options);
        output.Position = 0;

        _logger.LogInformation("Compressed image to {Bytes} bytes as {Ext}", output.Length, ext);
        return (output, contentType, ext);
    }

    private static void ResizeInPlace(Image image, int? maxW, int? maxH)
    {
        var w = image.Width;
        var h = image.Height;

        double scaleW = maxW.HasValue ? (double)maxW.Value / w : 1.0;
        double scaleH = maxH.HasValue ? (double)maxH.Value / h : 1.0;
        double scale = Math.Min(scaleW, scaleH);

        if (scale < 1.0)
        {
            int newW = Math.Max(1, (int)Math.Round(w * scale));
            int newH = Math.Max(1, (int)Math.Round(h * scale));
            image.Resize(newW, newH);
        }
    }

    private static (ImageOptionsBase options, string contentType, string ext) BuildOptions(string fmt, int? quality)
    {
        switch (fmt)
        {
            case "jpeg":
            {
                var q = quality ?? 80;
                var jpeg = new JpegOptions { Quality = q };
                return (jpeg, "image/jpeg", "jpg");
            }
            case "png":
            {
                // PNG is lossless; using defaults ensures broad compatibility.
                // Many PNG tunables exist, but defaults are safe and effective.
                var png = new PngOptions();
                return (png, "image/png", "png");
            }
            default:
                throw new ArgumentOutOfRangeException(nameof(fmt), "Unsupported format.");
        }
    }

    private static void TryStripMetadata(Image image)
    {
        try
        {
            // Not every format exposes EXIF/IPTC the same way; a best-effort clear:
            if (image is RasterImage raster)
            {
                raster.RemoveAllFonts();
                raster.SetPropertyItems(Array.Empty<PropertyItem>());
            }
        }
        catch
        {
            // Non-fatal; ignore if format doesn't support these operations
        }
    }
}

Notes

  • JpegOptions.Quality (1–100) controls lossy compression.
  • PNG defaults are typically fine for a first version; if you need extra-small PNGs, you can add advanced tuning later.
  • TryStripMetadata is a best-effort approach; metadata APIs vary by source format.

/Controllers/ImageController.cs

using ImageApi.Models;
using ImageApi.Services;
using Microsoft.AspNetCore.Mvc;

namespace ImageApi.Controllers;

[ApiController]
[Route("api/images")]
public sealed class ImageController : ControllerBase
{
    private static readonly HashSet<string> AllowedContentTypes = new(StringComparer.OrdinalIgnoreCase)
    {
        "image/jpeg", "image/png", "image/gif", "image/webp", "image/bmp", "image/tiff"
    };

    private readonly IImageCompressionService _svc;
    private readonly ILogger<ImageController> _logger;

    public ImageController(IImageCompressionService svc, ILogger<ImageController> logger)
    {
        _svc = svc;
        _logger = logger;
    }

    // POST /api/images/compress?format=jpeg&quality=75&maxWidth=1280&maxHeight=1280
    [HttpPost("compress")]
    [RequestSizeLimit(25_000_000)] // 25 MB cap; adjust to your needs
    public async Task<IActionResult> Compress(
        [FromQuery] string? format,
        [FromQuery] int? quality,
        [FromQuery] int? maxWidth,
        [FromQuery] int? maxHeight,
        [FromQuery] bool stripMetadata = true,
        IFormFile? file = null,
        CancellationToken ct = default)
    {
        if (file is null || file.Length == 0)
            return BadRequest("No file uploaded.");

        if (!AllowedContentTypes.Contains(file.ContentType))
            return BadRequest("Unsupported content type. Upload a common raster image (JPEG, PNG, GIF, WebP, BMP, TIFF).");

        var req = new CompressionRequest
        {
            Format = string.IsNullOrWhiteSpace(format) ? "jpeg" : format!,
            Quality = quality,
            MaxWidth = maxWidth,
            MaxHeight = maxHeight,
            StripMetadata = stripMetadata
        };

        await using var input = file.OpenReadStream();
        var (output, contentType, ext) = await _svc.CompressAsync(input, req, ct);

        // Strong caching for immutable responses (tune for your app/CDN)
        Response.Headers.CacheControl = "public,max-age=31536000,immutable";

        return File(output, contentType, fileDownloadName: BuildDownloadName(file.FileName, ext));
    }

    private static string BuildDownloadName(string originalName, string newExt)
    {
        var baseName = Path.GetFileNameWithoutExtension(originalName);
        return $"{baseName}-compressed.{newExt}";
    }
}

Program.cs (DI registration + optional license)

using Aspose.Imaging;
using ImageApi.Services;

var builder = WebApplication.CreateBuilder(args);

// Optional: initialize Aspose license from a file or stream if you have one
// var license = new Aspose.Imaging.License();
// license.SetLicense("Aspose.Total.lic");

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddSingleton<IImageCompressionService, ImageCompressionService>();

var app = builder.Build();

app.UseRouting();
app.UseAuthorization();
app.MapControllers();

// Enable for local testing
app.UseSwagger();
app.UseSwaggerUI();

app.Run();

Step-by-Step Guide

Step 1: Set Up the Project

Create an ASP.NET Core Web API project. Add the Aspose.Imaging NuGet package. Create the Models, Services, and Controllers folders as shown above.

Step 2: Configure Aspose.Imaging (optional licensing)

If you have a license, initialize it at startup (see Program.cs). This avoids evaluation watermarks and ensures full functionality.

Step 3: Implement the Compression Service

The ImageCompressionService:

  • Loads images via Image.Load(Stream)
  • Optionally strips metadata
  • Optionally resizes with aspect ratio preserved
  • Saves to JPEG or PNG with format-appropriate options

Step 4: Build the API Controller

ImageController exposes POST /api/images/compress taking a file and query parameters:

  • format: jpeg or png (default jpeg)
  • quality: 1–100 (JPEG only; default 80)
  • maxWidth/maxHeight: bounds for downscaling
  • stripMetadata: default true for smaller output

Step 5: Test the API

Use any HTTP client to send a multipart/form-data request with a single file field named file, plus optional query parameters. Verify:

  • Response Content-Type matches format
  • Returned file size is reduced
  • Resizing works as expected

Design Choices & Best Practices

  • Format-aware settings: JPEG uses Quality; PNG stays lossless for predictable output.
  • Downscale before encode: Resizing reduces pixels first (largest size wins), then encoding shrinks bytes further.
  • Sanitize inputs: Guard content type, file size, query bounds.
  • Streaming: Avoid reading the entire file into memory repeatedly; keep streams short-lived and seekable.
  • Caching: Mark responses as immutable if you derive name/content from deterministic inputs; otherwise tune cache headers to your use case.
  • Security: Validate content type and reject suspicious payloads. Consider scanning for malformed images.
  • Observability: Log sizes before/after and parameters used; this helps you tune defaults.
  • Throttling: If exposed publicly, rate-limit or require auth to prevent abuse.

Common Extensions (drop-in later)

  • WebP/AVIF encoders for even smaller images (add new options/contentType/file extension in BuildOptions).
  • PNG tuning (filtering/compression level) if you need extra-small lossless assets.
  • Preset profiles like thumbnail, preview, hires mapping to known parameters.
  • ETags or content hashing to serve identical responses from cache.
  • Async batch endpoint to compress multiple files at once.

Troubleshooting

  • Huge inputs: Increase RequestSizeLimit or stream to temp storage.
  • Wrong colors: Ensure colorspace is handled by defaults; advanced cases might need explicit color type.
  • No size reduction (PNG): PNG is lossless; enable resize or switch to JPEG for stronger byte savings.

Summary

You now have a production-ready dynamic image compression API using Aspose.Imaging. The controller handles uploads and parameters; the service applies safe, format-aware compression and optional resizing, then streams back a properly typed response with cache headers. From here, you can add more formats, presets, and caching strategies to fit your web stack.

More in this category