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
orpng
(defaultjpeg
)quality
: 1–100 (JPEG only; default 80)maxWidth
/maxHeight
: bounds for downscalingstripMetadata
: defaulttrue
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 inBuildOptions
). - 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
- Optimizing Animated GIFs in .NET using Aspose.Imaging
- Optimize Multi-Page TIFFs for Archival in .NET with Aspose
- Comparing Lossy vs. Lossless Image Compression in .NET using Aspose.Imaging
- Converting TIFF to PDF in C# with Aspose.Imaging
- Cropping Product Images for E-Commerce Platforms using Aspose.Imaging for .NET