Extending the Library: Implementing Your Own Image Providers¶
This guide explains the contracts behind the library's core interfaces and shows how to provide your own implementations — for example, to back the library with a different imaging engine (ImageMagick, ImageSharp, a cloud service, hardware codecs, etc.) — and how to wire them into your application's dependency injection container.
Table of Contents¶
- How the pieces fit together
- The contracts
IImageDecoderIImageHelperIImageQuantizerIEnhancedImageHelper- Registering your implementations
- Using the services in your solution
- Complete example
- Things to watch out for
How the pieces fit together¶
All image work flows through a small set of interfaces in the
dotnet_image_thumbnail.Library.Image namespace:
┌────────────────────────────┐
│ IImageThumbnailManager │ ← high-level entry point
│ (naming + caching) │
└─────────────┬──────────────┘
│ depends on
┌───────────────────┼───────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ IImageHelper │ │ IImageDecoder│ │IImageQuantizer│
│ (resize/save)│ │(format detect)│ │ (reduce colors)│
└──────────────┘ └──────────────┘ └──────────────┘
┌────────────────────┐
│ IEnhancedImageHelper│ ← optional, adds advanced ops
└────────────────────┘ (inherits IImageHelper)
IImageThumbnailManagerorchestrates the other three: it asks the decoder for the format, the helper to produce thumbnail bytes, and the quantizer to optimize them.IEnhancedImageHelperextendsIImageHelperwith advanced operations but is not consumed byIImageThumbnailManager; you use it directly in your own code.
Because every component depends only on these abstractions, you can swap any (or all) of them for your own implementations without touching the rest of the library.
The contracts¶
All interfaces live in dotnet_image_thumbnail.Library.Image.
IImageDecoder¶
Responsible for detecting the encoded format of an image from its file path or content.
public interface IImageDecoder
{
public enum EncodedImageFormat
{
Bmp, Gif, Ico, Jpg, Jpeg, Png, Wbmp, Webp, Avif
}
EncodedImageFormat? GetEncodedImageFormat(string filename);
}
What to implement
- Return the detected
EncodedImageFormat, ornullwhen the format is unknown or unsupported. - The
filenameargument is a path. You may decide by extension, by reading magic bytes from the file header, or both — but remember it may also be a path to a file that does not exist yet (see the note aboutSavebelow).
Example
using dotnet_image_thumbnail.Library.Image;
public sealed class MyImageDecoder : IImageDecoder
{
public IImageDecoder.EncodedImageFormat? GetEncodedImageFormat(string filename)
{
// Prefer content sniffing over extension for reliability.
return Path.GetExtension(filename).ToLowerInvariant() switch
{
".png" => IImageDecoder.EncodedImageFormat.Png,
".gif" => IImageDecoder.EncodedImageFormat.Gif,
".jpg" => IImageDecoder.EncodedImageFormat.Jpeg,
".jpeg" => IImageDecoder.EncodedImageFormat.Jpeg,
".bmp" => IImageDecoder.EncodedImageFormat.Bmp,
".webp" => IImageDecoder.EncodedImageFormat.Webp,
".avif" => IImageDecoder.EncodedImageFormat.Avif,
".ico" => IImageDecoder.EncodedImageFormat.Ico,
_ => null
};
}
}
IImageHelper¶
Performs the actual thumbnail creation and file saving.
public interface IImageHelper
{
byte[] CreateThumbnailWithVariableHeight(string filename, EncodedImageFormat? imageFormat, int? height);
byte[] CreateThumbnailWithVariableWidth(string filename, EncodedImageFormat? imageFormat, int? width);
bool Save(byte[] content, EncodedImageFormat imageFormat, string filename);
}
What to implement
- The two
CreateThumbnail*methods take a source file and a single target dimension, and must preserve the aspect ratio (the other dimension is derived from the source). Return the encoded thumbnail bytes. imageFormatis the desired output format; treatnullas "keep the source format".height/widthofnullmeans "keep the source dimension" — do not pass it through unhandled.Savewritescontenttofilenamein the given format and returns whether the write succeeded.
Example
using static dotnet_image_thumbnail.Library.Image.IImageDecoder;
public sealed class MyImageHelper : IImageHelper
{
public byte[] CreateThumbnailWithVariableHeight(string filename, EncodedImageFormat? imageFormat, int? height)
=> CreateThumbnail(filename, imageFormat, width: null, height);
public byte[] CreateThumbnailWithVariableWidth(string filename, EncodedImageFormat? imageFormat, int? width)
=> CreateThumbnail(filename, imageFormat, width, height: null);
private static byte[] CreateThumbnail(string filename, EncodedImageFormat? imageFormat, int? width, int? height)
{
// Use your imaging engine here. This is where the real work happens.
// 1. Decode `filename`.
// 2. Scale to `width` or `height`, keeping aspect ratio.
// 3. Encode to `imageFormat` (or the source format when null).
throw new NotImplementedException();
}
public bool Save(byte[] content, EncodedImageFormat imageFormat, string filename)
{
// Encode `content` into `imageFormat` and write to `filename`.
throw new NotImplementedException();
}
}
using static dotnet_image_thumbnail.Library.Image.IImageDecoder;lets you refer toEncodedImageFormatunqualified, matching the built-in implementations.
IImageQuantizer¶
Reduces the number of colors in an image (palette quantization) to shrink file size, primarily for palette-based formats such as GIF and PNG.
public interface IImageQuantizer
{
byte[] Quantize(byte[] image, IImageDecoder.EncodedImageFormat? format);
}
What to implement
- Accept encoded image bytes and return new encoded bytes with a reduced color palette.
formatis the input format; treatnullas "auto-detect".- The result must remain a valid image of the same format.
Example
public sealed class MyImageQuantizer : IImageQuantizer
{
public byte[] Quantize(byte[] image, IImageDecoder.EncodedImageFormat? format)
{
// Decode `image`, apply color reduction, and re-encode.
throw new NotImplementedException();
}
}
IEnhancedImageHelper¶
An optional superset of IImageHelper with advanced operations. It inherits the
three IImageHelper members, so an implementation must provide those too.
public interface IEnhancedImageHelper : IImageHelper
{
byte[] CreateThumbnailWithCrop(string filename, EncodedImageFormat? imageFormat, int width, int height);
byte[] ApplyImageFilters(byte[] imageData, EncodedImageFormat? format,
float brightness = 1.0f, float contrast = 1.0f, float saturation = 1.0f);
byte[] CreateThumbnailFromBytes(byte[] imageData, EncodedImageFormat? imageFormat, int width, int height);
EncodedImageFormat? DetectImageFormat(byte[] imageData);
byte[] ConvertImageFormat(byte[] imageData, EncodedImageFormat sourceFormat, EncodedImageFormat targetFormat);
}
What to implement
CreateThumbnailWithCrop— produce an exactwidth × heightimage, cropping (not stretching) to fill the dimensions.ApplyImageFilters— adjust brightness/contrast/saturation.1.0fis the neutral value for each.CreateThumbnailFromBytes— likeCreateThumbnail*, but the source is in-memory bytes instead of a file path.DetectImageFormat— sniff the format from raw bytes (no file involved).ConvertImageFormat— transcode between two known formats.
Example
public sealed class MyEnhancedImageHelper : IEnhancedImageHelper
{
public byte[] CreateThumbnailWithVariableHeight(string filename, EncodedImageFormat? imageFormat, int? height)
=> throw new NotImplementedException();
public byte[] CreateThumbnailWithVariableWidth(string filename, EncodedImageFormat? imageFormat, int? width)
=> throw new NotImplementedException();
public bool Save(byte[] content, EncodedImageFormat imageFormat, string filename)
=> throw new NotImplementedException();
public byte[] CreateThumbnailWithCrop(string filename, EncodedImageFormat? imageFormat, int width, int height)
=> throw new NotImplementedException();
public byte[] ApplyImageFilters(byte[] imageData, EncodedImageFormat? format,
float brightness = 1.0f, float contrast = 1.0f, float saturation = 1.0f)
=> throw new NotImplementedException();
public byte[] CreateThumbnailFromBytes(byte[] imageData, EncodedImageFormat? imageFormat, int width, int height)
=> throw new NotImplementedException();
public EncodedImageFormat? DetectImageFormat(byte[] imageData)
=> throw new NotImplementedException();
public byte[] ConvertImageFormat(byte[] imageData, EncodedImageFormat sourceFormat, EncodedImageFormat targetFormat)
=> throw new NotImplementedException();
}
Registering your implementations¶
Registration happens through the extension method
AddImageServices in dotnet_image_thumbnail.Library.Image.Configuration.
The library registers its built-in implementations with TryAddScoped, which only
adds a registration when the service type is not already registered. That gives
you a clean extension point: register your own implementations first, then call
AddImageServices — your versions win, and any pieces you did not override are filled
in by the library.
Option A — override some or all pieces (recommended)¶
using dotnet_image_thumbnail.Library.Image;
using dotnet_image_thumbnail.Library.Image.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
services.TryAddScoped<IImageDecoder, MyImageDecoder>();
services.TryAddScoped<IImageHelper, MyImageHelper>();
services.TryAddScoped<IImageQuantizer, MyImageQuantizer>();
// Optional: expose the advanced surface too.
services.TryAddScoped<IEnhancedImageHelper, MyEnhancedImageHelper>();
// Registers IImageThumbnailManager and any missing built-in pieces.
// Your pre-registered implementations are preserved.
services.AddImageServices();
You can override just one interface — e.g. only IImageHelper — and let the library
supply the decoder and quantizer. Everything is mixed and matched through the
abstractions.
Option B — register everything manually¶
Skip AddImageServices entirely and register the full graph yourself:
services.TryAddScoped<IImageDecoder, MyImageDecoder>();
services.TryAddScoped<IImageHelper, MyImageHelper>();
services.TryAddScoped<IImageQuantizer, MyImageQuantizer>();
services.TryAddScoped<IEnhancedImageHelper, MyEnhancedImageHelper>();
services.TryAddScoped<IImageThumbnailManager, ImageThumbnailManager>();
If you register
IEnhancedImageHelper, remember thatIImageThumbnailManagerdepends on the baseIImageHelper. To have the manager use your enhanced implementation, register it forIImageHelperas well (or instead).
Lifetime guidance¶
- The built-in services are registered as scoped. Match that unless you have a reason not to.
- Prefer stateless, thread-safe implementations. Do not store per-request state in the implementations themselves, or a scoped/singleton instance may leak state between requests.
Using the services in your solution¶
Inject the high-level manager and let it handle naming, caching, and regeneration:
public sealed class ThumbnailService
{
private readonly IImageThumbnailManager _manager;
public ThumbnailService(IImageThumbnailManager manager) => _manager = manager;
public string GetThumbnail(string imagePath)
=> _manager.RetrieveThumbnailFileName(imagePath, thumbnailWidth: 300, thumbnailHeight: null);
}
Or inject individual interfaces when you need finer control:
public sealed class ImageService
{
private readonly IImageDecoder _decoder;
private readonly IImageHelper _helper;
private readonly IEnhancedImageHelper _enhanced;
public ImageService(IImageDecoder decoder, IImageHelper helper, IEnhancedImageHelper enhanced)
{
_decoder = decoder;
_helper = helper;
_enhanced = enhanced;
}
public byte[] Crop(string path, int width, int height)
{
var format = _decoder.GetEncodedImageFormat(path);
return _enhanced.CreateThumbnailWithCrop(path, format, width, height);
}
}
Complete example¶
A minimal but complete wiring showing a custom engine being plugged in:
using dotnet_image_thumbnail.Library.Image;
using dotnet_image_thumbnail.Library.Image.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
var services = new ServiceCollection();
// 1. Your implementations first (they win over the library's TryAddScoped).
services.TryAddScoped<IImageDecoder, MyImageDecoder>();
services.TryAddScoped<IImageHelper, MyImageHelper>();
services.TryAddScoped<IImageQuantizer, MyImageQuantizer>();
services.TryAddScoped<IEnhancedImageHelper, MyEnhancedImageHelper>();
// 2. Library registrations fill in the manager (and anything you skipped).
services.AddImageServices();
await using var provider = services.BuildServiceProvider();
// 3. Use it.
var manager = provider.GetRequiredService<IImageThumbnailManager>();
string thumbnail = manager.RetrieveThumbnailFileName("photos/beach.jpg", 320, null);
Things to watch out for¶
nullformat arguments.imageFormat/formatofnullmeans "detect or keep the original format". Handle it explicitly rather than passing it to your engine.Savemay target a path that does not exist yet.IImageThumbnailManagercan callSaveon a not-yet-created thumbnail path; a decoder that only reads file content must not assume the file exists when called for the destination.- Aspect ratio.
CreateThumbnailWithVariableWidth/Heightmust preserve the source aspect ratio; onlyCreateThumbnailWithCropshould produce exact dimensions by cropping. - Encoded bytes in, encoded bytes out. The helper and quantizer work on encoded
byte[], not on engine-specific image objects. Do your decode/encode internally. - Thread safety. Assume scoped lifetimes and concurrent use; keep implementations stateless or synchronize any shared resources.
- Return values.
Saveshould returnfalseon failure rather than throwing, where possible;GetEncodedImageFormatreturnsnullfor unknown/unsupported formats.