From 3293a3f3013bd94160b42c19100c9de72858f79f Mon Sep 17 00:00:00 2001 From: Gabriel Sales Date: Fri, 29 May 2026 15:54:28 -0300 Subject: [PATCH 1/2] Add openfile from url --- README.md | 3 ++ docs/getting-started.md | 23 ++++++++++ .../Components/Pages/Home.razor | 44 +++++++++++++++++++ .../VisionSharp.BlazorDemo/wwwroot/app.css | 23 +++++++++- samples/VisionSharp.ConsoleDemo/Program.cs | 25 ++++++++++- .../Interfaces/IImageFactory.cs | 3 ++ .../ServicedImageFactory.cs | 12 +++++ src/VisionSharp/ImageFactory.cs | 12 +++++ 8 files changed, 142 insertions(+), 3 deletions(-) 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); + }); + } } From 4ce754952cb450b96f034b7b244d2fbe27ae3d79 Mon Sep 17 00:00:00 2001 From: Gabriel Sales Date: Fri, 29 May 2026 16:00:50 -0300 Subject: [PATCH 2/2] Add tests --- .../ImageFactoryTests.cs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) 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(); + } }