diff --git a/README.md b/README.md
index 7501483..8d257e5 100644
--- a/README.md
+++ b/README.md
@@ -68,6 +68,9 @@ ImageFactory.OpenAsync(bytes)
// Base64
ImageFactory.OpenBase64Async(base64String)
+
+// URL (download is deferred until the terminal call)
+ImageFactory.OpenAsync(new Uri("https://example.com/photo.jpg"))
```
### Resize
diff --git a/docs/getting-started.md b/docs/getting-started.md
index 075c1eb..deb759f 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -24,6 +24,29 @@ var image = await ImageFactory
Console.WriteLine($"Saved: {image.Width}×{image.Height}");
```
+## Loading from a URL
+
+`OpenAsync` also accepts a `Uri`. The HTTP download happens lazily, at the same moment the rest of the pipeline executes (when you call a terminal method).
+
+```csharp
+var image = await ImageFactory
+ .OpenAsync(new Uri("https://example.com/photo.jpg"))
+ .Resize(800, 600)
+ .ToJpeg(85)
+ .SaveAsync("output.jpg");
+```
+
+Pass a `CancellationToken` to any terminal method to cancel both the download and the processing:
+
+```csharp
+using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
+
+var bytes = await ImageFactory
+ .OpenAsync(new Uri("https://example.com/photo.jpg"))
+ .Grayscale()
+ .ToBytesAsync(cts.Token);
+```
+
## Understanding the pipeline
`ImageFactory.OpenAsync(...)` returns an `IImageBuilder`. Each method call on the
diff --git a/samples/VisionSharp.BlazorDemo/Components/Pages/Home.razor b/samples/VisionSharp.BlazorDemo/Components/Pages/Home.razor
index a3027fa..96c35ab 100644
--- a/samples/VisionSharp.BlazorDemo/Components/Pages/Home.razor
+++ b/samples/VisionSharp.BlazorDemo/Components/Pages/Home.razor
@@ -28,6 +28,17 @@
Replace image
+
+
+
+
+ @if (_urlError is not null)
+ {
+
@_urlError
+ }
@@ -293,6 +304,10 @@
private static readonly string[] OutputFormats = ["jpeg", "png", "webp"];
+ private string _urlInput = string.Empty;
+ private bool _urlLoading;
+ private string? _urlError;
+
private CancellationTokenSource? _cts;
private Timer? _debounce;
@@ -335,6 +350,35 @@
await ProcessAsync();
}
+ // ── URL load ─────────────────────────────────────────────────────────────
+ private async Task LoadFromUrlAsync()
+ {
+ _urlError = null;
+ if (!Uri.TryCreate(_urlInput, UriKind.Absolute, out var uri) ||
+ (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps))
+ {
+ _urlError = "Please enter a valid HTTP or HTTPS URL.";
+ return;
+ }
+
+ _urlLoading = true;
+ StateHasChanged();
+ try
+ {
+ _originalBytes = await ImageFactory.OpenAsync(uri).ToBytesAsync();
+ await ProcessAsync();
+ }
+ catch (Exception ex)
+ {
+ _urlError = $"Could not load image: {ex.Message}";
+ }
+ finally
+ {
+ _urlLoading = false;
+ StateHasChanged();
+ }
+ }
+
// ── Slider / text handlers (debounced) ───────────────────────────────────
private void OnSlider(ChangeEventArgs e, Action set)
{
diff --git a/samples/VisionSharp.BlazorDemo/wwwroot/app.css b/samples/VisionSharp.BlazorDemo/wwwroot/app.css
index 239e650..a04d956 100644
--- a/samples/VisionSharp.BlazorDemo/wwwroot/app.css
+++ b/samples/VisionSharp.BlazorDemo/wwwroot/app.css
@@ -130,7 +130,28 @@ html, body { height: 100%; background: var(--bg); color: var(--text); font-famil
padding: .5rem .9rem;
background: var(--surface2);
display: flex;
- justify-content: flex-end;
+ flex-direction: column;
+ align-items: flex-end;
+ gap: .4rem;
+}
+
+.url-row {
+ display: flex;
+ gap: .4rem;
+ align-items: center;
+ width: 100%;
+}
+
+.url-input {
+ flex: 1;
+ min-width: 0;
+}
+
+.url-error {
+ font-size: .75rem;
+ color: #e05c5c;
+ margin: 0;
+ width: 100%;
}
/* ── Controls grid ───────────────────────────────────────────────────────── */
diff --git a/samples/VisionSharp.ConsoleDemo/Program.cs b/samples/VisionSharp.ConsoleDemo/Program.cs
index c477129..d8166e0 100644
--- a/samples/VisionSharp.ConsoleDemo/Program.cs
+++ b/samples/VisionSharp.ConsoleDemo/Program.cs
@@ -56,7 +56,7 @@
continue;
}
- if (int.TryParse(input, out int choice) && choice >= 1 && choice <= 15)
+ if (int.TryParse(input, out int choice) && choice >= 1 && choice <= 16)
await RunDemoAsync(choice, imagePath);
else
WriteError("Invalid option. Type a number from the menu, 0 for all, i to change image, or q to quit.");
@@ -96,6 +96,7 @@ static void PrintMenu()
Console.WriteLine(" 13 Stream I/O");
Console.WriteLine(" 14 Vintage preset");
Console.WriteLine(" 15 Black & White preset");
+ Console.WriteLine(" 16 Load from URL");
Console.ForegroundColor = ConsoleColor.DarkCyan;
Console.WriteLine(" ─────────────────────────────────────────────");
Console.ResetColor();
@@ -122,7 +123,7 @@ static async Task RunAllDemosAsync(string imagePath)
Console.ResetColor();
var sw = Stopwatch.StartNew();
- for (int i = 1; i <= 15; i++)
+ for (int i = 1; i <= 16; i++)
await RunDemoAsync(i, imagePath);
sw.Stop();
@@ -262,6 +263,17 @@ await ImageFactory.OpenAsync(imagePath)
await ImageFactory.OpenAsync(imagePath).BlackAndWhite().SaveAsync("output/demo_bw.jpg");
WriteOk("output/demo_bw.jpg", sw);
break;
+
+ case 16:
+ string url = AskUrl(" Image URL (Enter for demo URL): ", "https://picsum.photos/800/600");
+ Console.Write($" [16] Load from URL → Resize → Save … ");
+ await ImageFactory
+ .OpenAsync(new Uri(url))
+ .Resize(800, 600)
+ .ToJpeg(90)
+ .SaveAsync("output/demo_from_url.jpg");
+ WriteOk("output/demo_from_url.jpg", sw);
+ break;
}
}
catch (Exception ex)
@@ -296,6 +308,15 @@ static int AskInt(string prompt, int defaultValue, int min, int max)
return defaultValue;
}
+static string AskUrl(string prompt, string defaultValue)
+{
+ Console.WriteLine();
+ Console.Write($" {prompt}");
+ string? raw = Console.ReadLine()?.Trim();
+ if (!string.IsNullOrEmpty(raw) && Uri.TryCreate(raw, UriKind.Absolute, out _)) return raw;
+ return defaultValue;
+}
+
static float AskFloat(string prompt, float defaultValue)
{
Console.WriteLine();
diff --git a/src/VisionSharp.Abstractions/Interfaces/IImageFactory.cs b/src/VisionSharp.Abstractions/Interfaces/IImageFactory.cs
index 7a4af42..a151ba4 100644
--- a/src/VisionSharp.Abstractions/Interfaces/IImageFactory.cs
+++ b/src/VisionSharp.Abstractions/Interfaces/IImageFactory.cs
@@ -17,4 +17,7 @@ public interface IImageFactory
/// Opens an image from a Base64-encoded string.
IImageBuilder OpenBase64Async(string base64);
+
+ /// Opens an image from a URL. The download is deferred to the terminal call.
+ IImageBuilder OpenAsync(Uri url);
}
diff --git a/src/VisionSharp/DependencyInjection/ServicedImageFactory.cs b/src/VisionSharp/DependencyInjection/ServicedImageFactory.cs
index 865855f..0172c46 100644
--- a/src/VisionSharp/DependencyInjection/ServicedImageFactory.cs
+++ b/src/VisionSharp/DependencyInjection/ServicedImageFactory.cs
@@ -9,6 +9,7 @@ namespace VisionSharp.DependencyInjection;
///
internal sealed class ServicedImageFactory : IImageFactory
{
+ private static readonly HttpClient _httpClient = new();
private readonly Func _engineFactory;
public ServicedImageFactory(Func engineFactory)
@@ -31,4 +32,15 @@ public IImageBuilder OpenAsync(byte[] bytes)
///
public IImageBuilder OpenBase64Async(string base64)
=> new ImageBuilder(_engineFactory, (e, ct) => e.LoadFromBase64Async(base64, ct));
+
+ ///
+ public IImageBuilder OpenAsync(Uri url)
+ {
+ ArgumentNullException.ThrowIfNull(url);
+ return new ImageBuilder(_engineFactory, async (e, ct) =>
+ {
+ var bytes = await _httpClient.GetByteArrayAsync(url, ct);
+ await e.LoadFromBytesAsync(bytes, ct);
+ });
+ }
}
diff --git a/src/VisionSharp/ImageFactory.cs b/src/VisionSharp/ImageFactory.cs
index d90f6b8..916ce66 100644
--- a/src/VisionSharp/ImageFactory.cs
+++ b/src/VisionSharp/ImageFactory.cs
@@ -22,6 +22,7 @@ namespace VisionSharp;
public static class ImageFactory
{
private static Func _engineFactory = () => new ImageSharpEngine();
+ private static readonly HttpClient _httpClient = new();
///
/// Overrides the engine factory used to create engine instances.
@@ -57,4 +58,15 @@ public static IImageBuilder OpenBase64Async(string base64)
ArgumentException.ThrowIfNullOrWhiteSpace(base64);
return new ImageBuilder(_engineFactory, (engine, ct) => engine.LoadFromBase64Async(base64, ct));
}
+
+ /// Opens an image from a URL. The download is deferred to the terminal call.
+ public static IImageBuilder OpenAsync(Uri url)
+ {
+ ArgumentNullException.ThrowIfNull(url);
+ return new ImageBuilder(_engineFactory, async (engine, ct) =>
+ {
+ var bytes = await _httpClient.GetByteArrayAsync(url, ct);
+ await engine.LoadFromBytesAsync(bytes, ct);
+ });
+ }
}
diff --git a/tests/VisionSharp.UnitTests/ImageFactoryTests.cs b/tests/VisionSharp.UnitTests/ImageFactoryTests.cs
index 4a687db..ef9ad1d 100644
--- a/tests/VisionSharp.UnitTests/ImageFactoryTests.cs
+++ b/tests/VisionSharp.UnitTests/ImageFactoryTests.cs
@@ -83,4 +83,41 @@ public void OpenAsync_ReturnsSameTypeAsImageBuilder()
builder.Should().BeOfType();
}
+
+ [Fact]
+ public void OpenAsync_WithUri_ReturnsImageBuilder()
+ {
+ var uri = new Uri("https://example.com/photo.jpg");
+ var builder = ImageFactory.OpenAsync(uri);
+
+ builder.Should().NotBeNull();
+ builder.Should().BeAssignableTo();
+ }
+
+ [Fact]
+ public void OpenAsync_WithUri_ReturnsSameTypeAsImageBuilder()
+ {
+ var uri = new Uri("https://example.com/photo.jpg");
+ var builder = ImageFactory.OpenAsync(uri);
+
+ builder.Should().BeOfType();
+ }
+
+ [Fact]
+ public void OpenAsync_NullUri_ThrowsArgumentNullException()
+ {
+ var act = () => ImageFactory.OpenAsync((Uri)null!);
+
+ act.Should().Throw();
+ }
+
+ [Fact]
+ public void OpenAsync_WithRelativeUri_ReturnsImageBuilder()
+ {
+ var uri = new Uri("/images/photo.jpg", UriKind.Relative);
+ var builder = ImageFactory.OpenAsync(uri);
+
+ builder.Should().NotBeNull();
+ builder.Should().BeAssignableTo();
+ }
}