From f66ad9c4a18bed80f739a7041435f471b709f151 Mon Sep 17 00:00:00 2001 From: Federico Arambarri Date: Tue, 16 Jun 2026 16:54:04 -0300 Subject: [PATCH 1/5] Refresh --- .../ImageProcessingPipeline/ImageProcessingPipeline.csproj | 3 ++- pipes-and-filters/ImageProcessingPipeline/Watermark.cs | 5 +++-- pipes-and-filters/ImageProcessingPipeline/sixlabors.lic | 1 + pipes-and-filters/bicep/main.bicep | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) create mode 100644 pipes-and-filters/ImageProcessingPipeline/sixlabors.lic diff --git a/pipes-and-filters/ImageProcessingPipeline/ImageProcessingPipeline.csproj b/pipes-and-filters/ImageProcessingPipeline/ImageProcessingPipeline.csproj index da9db8cd..218559ee 100644 --- a/pipes-and-filters/ImageProcessingPipeline/ImageProcessingPipeline.csproj +++ b/pipes-and-filters/ImageProcessingPipeline/ImageProcessingPipeline.csproj @@ -6,6 +6,7 @@ enable enable true + Id=ctm_01kv8yyds8c8werqf8r2yd4f16;Kind=Community;ExpiryDateUtc=2027-09-14T19:36:16.0543789Z;Key=prjd1V+OYJjnSCvwlPcaA4LXZNSwyONd6SRFfA66H1hiMdI5X0gfNzhr8rbAw0awCqC18IboCcD6/4XbQ7m3KQ== @@ -14,7 +15,7 @@ - + diff --git a/pipes-and-filters/ImageProcessingPipeline/Watermark.cs b/pipes-and-filters/ImageProcessingPipeline/Watermark.cs index 8afbe2c0..7f3a5273 100644 --- a/pipes-and-filters/ImageProcessingPipeline/Watermark.cs +++ b/pipes-and-filters/ImageProcessingPipeline/Watermark.cs @@ -26,8 +26,9 @@ public async Task RunAsync( using BlobDownloadStreamingResult imageBlobContents = await imageBlob.DownloadStreamingAsync(null, cancellationToken); using var image = await Image.LoadAsync(imageBlobContents.Content, cancellationToken); - var resources = _files.GetDirectoryContents("/"); - using var watermarkStream = resources.First(resource => resource.Name.Equals("resources/watermark.png")).CreateReadStream(); + var resourcesDir = _files.GetDirectoryContents("resources"); + var watermarkFile = resourcesDir.First(resource => resource.Name.Equals("watermark.png")); + using var watermarkStream = watermarkFile.CreateReadStream(); using var watermarkImage = await Image.LoadAsync(watermarkStream, cancellationToken); image.Mutate(i => diff --git a/pipes-and-filters/ImageProcessingPipeline/sixlabors.lic b/pipes-and-filters/ImageProcessingPipeline/sixlabors.lic new file mode 100644 index 00000000..73f25259 --- /dev/null +++ b/pipes-and-filters/ImageProcessingPipeline/sixlabors.lic @@ -0,0 +1 @@ +Id=ctm_01kv8yyds8c8werqf8r2yd4f16;Kind=Community;ExpiryDateUtc=2050-09-14T19:36:16.0543789Z;Key=prjd1V+OYJjnSCvwlPcaA4LXZNSwyONd6SRFfA66H1hiMdI5X0gfNzhr8rbAw0awCqC18IboCcD6/4XbQ7m3KQ== \ No newline at end of file diff --git a/pipes-and-filters/bicep/main.bicep b/pipes-and-filters/bicep/main.bicep index b26e28b9..f06c365d 100644 --- a/pipes-and-filters/bicep/main.bicep +++ b/pipes-and-filters/bicep/main.bicep @@ -29,7 +29,7 @@ resource storageQueueDataContributorRole 'Microsoft.Authorization/roleDefinition /*** NEW RESOURCES ***/ @description('The Azure Storage account which will contain the pipes (queues) and the images to be sent through the filters (Azure Functions).') -resource storageAccount 'Microsoft.Storage/storageAccounts@2025-06-01' = { +resource storageAccount 'Microsoft.Storage/storageAccounts@2026-04-01' = { name: storageAccountName location: location sku: { From 4ad3ffff8057bb0067a6bd18c7b1fda1648c4e73 Mon Sep 17 00:00:00 2001 From: Federico Arambarri Date: Thu, 18 Jun 2026 09:35:46 -0300 Subject: [PATCH 2/5] Change library --- .../ImageProcessingPipeline.csproj | 13 ++- .../ImageProcessingPipeline/Program.cs | 3 - .../ImageProcessingPipeline/Resize.cs | 67 ++++++++++----- .../ImageProcessingPipeline/Watermark.cs | 85 ++++++++++++++----- .../ImageProcessingPipeline/sixlabors.lic | 1 - 5 files changed, 114 insertions(+), 55 deletions(-) delete mode 100644 pipes-and-filters/ImageProcessingPipeline/sixlabors.lic diff --git a/pipes-and-filters/ImageProcessingPipeline/ImageProcessingPipeline.csproj b/pipes-and-filters/ImageProcessingPipeline/ImageProcessingPipeline.csproj index 218559ee..02b4fbf3 100644 --- a/pipes-and-filters/ImageProcessingPipeline/ImageProcessingPipeline.csproj +++ b/pipes-and-filters/ImageProcessingPipeline/ImageProcessingPipeline.csproj @@ -5,8 +5,6 @@ Exe enable enable - true - Id=ctm_01kv8yyds8c8werqf8r2yd4f16;Kind=Community;ExpiryDateUtc=2027-09-14T19:36:16.0543789Z;Key=prjd1V+OYJjnSCvwlPcaA4LXZNSwyONd6SRFfA66H1hiMdI5X0gfNzhr8rbAw0awCqC18IboCcD6/4XbQ7m3KQ== @@ -14,8 +12,8 @@ - - + + @@ -30,9 +28,8 @@ - - - Always - + + PreserveNewest + \ No newline at end of file diff --git a/pipes-and-filters/ImageProcessingPipeline/Program.cs b/pipes-and-filters/ImageProcessingPipeline/Program.cs index 997fc0b4..54590909 100644 --- a/pipes-and-filters/ImageProcessingPipeline/Program.cs +++ b/pipes-and-filters/ImageProcessingPipeline/Program.cs @@ -1,7 +1,5 @@ using Azure.Identity; using Microsoft.Extensions.Azure; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Hosting; var host = new HostBuilder() @@ -13,7 +11,6 @@ c.UseCredential(new DefaultAzureCredential()); c.AddBlobServiceClient(hostContext.Configuration.GetSection("output")).WithName("processed"); }); - services.AddSingleton(new ManifestEmbeddedFileProvider(typeof(Program).Assembly)); }) .Build(); diff --git a/pipes-and-filters/ImageProcessingPipeline/Resize.cs b/pipes-and-filters/ImageProcessingPipeline/Resize.cs index 54a67c2d..06aecefb 100644 --- a/pipes-and-filters/ImageProcessingPipeline/Resize.cs +++ b/pipes-and-filters/ImageProcessingPipeline/Resize.cs @@ -2,15 +2,14 @@ using Azure.Storage.Blobs.Specialized; using Microsoft.Azure.Functions.Worker; using Microsoft.Extensions.Logging; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Processing; +using SkiaSharp; namespace ImageProcessingPipeline { - public class Resize(ILogger logger) { private readonly ILogger _logger = logger; + private const int MaxDimension = 600; [Function(nameof(Resize))] [QueueOutput("pipe-fjur", Connection = "pipe")] @@ -21,27 +20,55 @@ public async Task RunAsync( { _logger.LogInformation("Processing image {uri} for resizing.", imageBlob.Uri); - // Download image and resize it - using BlobDownloadStreamingResult imageBlobContents = await imageBlob.DownloadStreamingAsync(null, cancellationToken); - using var image = await Image.LoadAsync(imageBlobContents.Content, cancellationToken); - image.Mutate(i => + try { - i.Resize(new ResizeOptions + // Download image into memory (blob streams are not seekable for SkiaSharp) + using BlobDownloadStreamingResult imageBlobContents = await imageBlob.DownloadStreamingAsync(null, cancellationToken); + using var memoryStream = new MemoryStream(); + await imageBlobContents.Content.CopyToAsync(memoryStream, cancellationToken); + memoryStream.Position = 0; + + using var data = SKData.Create(memoryStream); + using var original = SKBitmap.Decode(data); + + // Validate image decode succeeded + if (original.Width <= 0 || original.Height <= 0) + { + _logger.LogError("Failed to decode image {filePath}: invalid dimensions {width}x{height}", imageFilePath, original.Width, original.Height); + throw new InvalidOperationException($"Image decode failed or image is empty: {imageFilePath}"); + } + + // Calculate resize scale (constrain to MaxDimension, don't upscale) + float scale = Math.Min( + (float)MaxDimension / original.Width, + (float)MaxDimension / original.Height); + scale = Math.Min(scale, 1.0f); // Don't upscale + + int newWidth = (int)(original.Width * scale); + int newHeight = (int)(original.Height * scale); + + _logger.LogDebug("Resizing image from {originalWidth}x{originalHeight} to {newWidth}x{newHeight}", original.Width, original.Height, newWidth, newHeight); + + // Resize and encode + using var resized = original.Resize(new SKImageInfo(newWidth, newHeight), SKSamplingOptions.Default); + using var resizedImage = SKImage.FromBitmap(resized); + using var encoded = resizedImage.Encode(SKEncodedImageFormat.Png, 100); + + // Write back to blob storage + _logger.LogDebug("Writing resized image back to storage: {uri}.", imageBlob.Uri); + using (var blobStream = await imageBlob.OpenWriteAsync(overwrite: true, cancellationToken: cancellationToken)) { - Mode = ResizeMode.Max, - Size = new Size(600, 600) - }); - }); - - // Write modified image back to storage - _logger.LogDebug("Writing resized image back to storage: {uri}.", imageBlob.Uri); - using (var blobStream = await imageBlob.OpenWriteAsync(overwrite: true, null, cancellationToken)) + encoded.SaveTo(blobStream); + } + + _logger.LogInformation("Image resizing done. Adding image \"{filePath}\" into the next pipe.", imageFilePath); + return imageFilePath; + } + catch (Exception ex) { - await image.SaveAsync(blobStream, image.Metadata.DecodedImageFormat!, cancellationToken); + _logger.LogError(ex, "Error resizing image {filePath}: {message}", imageFilePath, ex.Message); + throw; // Let the queue retry mechanism handle retries } - - _logger.LogInformation("Image resizing done. Adding image \"{filePath}\" into the next pipe.", imageFilePath); - return imageFilePath; } } } diff --git a/pipes-and-filters/ImageProcessingPipeline/Watermark.cs b/pipes-and-filters/ImageProcessingPipeline/Watermark.cs index 7f3a5273..db9cb578 100644 --- a/pipes-and-filters/ImageProcessingPipeline/Watermark.cs +++ b/pipes-and-filters/ImageProcessingPipeline/Watermark.cs @@ -1,17 +1,15 @@ using Azure.Storage.Blobs.Models; using Azure.Storage.Blobs.Specialized; using Microsoft.Azure.Functions.Worker; -using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Processing; +using SkiaSharp; namespace ImageProcessingPipeline { - public class Watermark(ILogger logger, IFileProvider files) + public class Watermark(ILogger logger) { private readonly ILogger _logger = logger; - private readonly IFileProvider _files = files; + private const byte WatermarkAlpha = 128; // 50% opacity [Function(nameof(Watermark))] [QueueOutput("pipe-yhrb", Connection = "pipe")] @@ -22,29 +20,70 @@ public async Task RunAsync( { _logger.LogInformation("Processing image {uri} for watermarking.", imageBlob.Uri); - // Download image and watermark it - using BlobDownloadStreamingResult imageBlobContents = await imageBlob.DownloadStreamingAsync(null, cancellationToken); - using var image = await Image.LoadAsync(imageBlobContents.Content, cancellationToken); + try + { + // Download image into memory (blob streams are not seekable for SkiaSharp) + using BlobDownloadStreamingResult imageBlobContents = await imageBlob.DownloadStreamingAsync(null, cancellationToken); + using var memoryStream = new MemoryStream(); + await imageBlobContents.Content.CopyToAsync(memoryStream, cancellationToken); + memoryStream.Position = 0; - var resourcesDir = _files.GetDirectoryContents("resources"); - var watermarkFile = resourcesDir.First(resource => resource.Name.Equals("watermark.png")); - using var watermarkStream = watermarkFile.CreateReadStream(); - using var watermarkImage = await Image.LoadAsync(watermarkStream, cancellationToken); + using var data = SKData.Create(memoryStream); + using var original = SKBitmap.Decode(data); - image.Mutate(i => - { - i.DrawImage(watermarkImage, new Point((image.Width - watermarkImage.Width) / 2, (image.Height - watermarkImage.Height) / 2), 0.5f); - }); + // Validate image decode succeeded + if (original.Width <= 0 || original.Height <= 0) + { + _logger.LogError("Failed to decode image {filePath}: invalid dimensions {width}x{height}", imageFilePath, original.Width, original.Height); + throw new InvalidOperationException($"Image decode failed or image is empty: {imageFilePath}"); + } + + // Load watermark from resources directory + var watermarkPath = Path.Combine(AppContext.BaseDirectory, "resources", "watermark.png"); + if (!File.Exists(watermarkPath)) + { + _logger.LogError("Watermark file not found at {path}", watermarkPath); + throw new FileNotFoundException($"Watermark file not found: {watermarkPath}"); + } + + using var watermarkBitmap = SKBitmap.Decode(watermarkPath); + + // Validate watermark fits in image + if (watermarkBitmap.Width > original.Width || watermarkBitmap.Height > original.Height) + { + _logger.LogWarning("Watermark {watermarkWidth}x{watermarkHeight} is larger than image {imageWidth}x{imageHeight}. Centering anyway.", + watermarkBitmap.Width, watermarkBitmap.Height, original.Width, original.Height); + } - // Write modified image back to storage - _logger.LogDebug("Writing watermarked image back to storage: {uri}.", imageBlob.Uri); - using (var blobStream = await imageBlob.OpenWriteAsync(overwrite: true, null, cancellationToken)) + // Draw original image and overlay watermark at 50% opacity + using var surface = SKSurface.Create(new SKImageInfo(original.Width, original.Height)); + var canvas = surface.Canvas; + canvas.DrawBitmap(original, 0, 0); + + // Create watermark with transparency + int wmX = (original.Width - watermarkBitmap.Width) / 2; + int wmY = (original.Height - watermarkBitmap.Height) / 2; + using var wmPaint = new SKPaint { Color = SKColors.White.WithAlpha(WatermarkAlpha) }; + canvas.DrawBitmap(watermarkBitmap, wmX, wmY, wmPaint); + canvas.Flush(); + + // Encode and write back to storage + _logger.LogDebug("Writing watermarked image back to storage: {uri}.", imageBlob.Uri); + using var resultImage = surface.Snapshot(); + using var encoded = resultImage.Encode(SKEncodedImageFormat.Png, 100); + using (var blobStream = await imageBlob.OpenWriteAsync(overwrite: true, cancellationToken: cancellationToken)) + { + encoded.SaveTo(blobStream); + } + + _logger.LogInformation("Watermarking done. Adding image \"{filePath}\" into the next pipe.", imageFilePath); + return imageFilePath; + } + catch (Exception ex) { - await image.SaveAsync(blobStream, image.Metadata.DecodedImageFormat!, cancellationToken); + _logger.LogError(ex, "Error watermarking image {filePath}: {message}", imageFilePath, ex.Message); + throw; // Let the queue retry mechanism handle retries } - - _logger.LogInformation("Watermarking done. Adding image \"{filePath}\" into the next pipe.", imageFilePath); - return imageFilePath; } } } diff --git a/pipes-and-filters/ImageProcessingPipeline/sixlabors.lic b/pipes-and-filters/ImageProcessingPipeline/sixlabors.lic deleted file mode 100644 index 73f25259..00000000 --- a/pipes-and-filters/ImageProcessingPipeline/sixlabors.lic +++ /dev/null @@ -1 +0,0 @@ -Id=ctm_01kv8yyds8c8werqf8r2yd4f16;Kind=Community;ExpiryDateUtc=2050-09-14T19:36:16.0543789Z;Key=prjd1V+OYJjnSCvwlPcaA4LXZNSwyONd6SRFfA66H1hiMdI5X0gfNzhr8rbAw0awCqC18IboCcD6/4XbQ7m3KQ== \ No newline at end of file From 5186a71ebe89a7c88b049c836bfa268f7797b4b5 Mon Sep 17 00:00:00 2001 From: "Federico Arambarri (CLARIUS CONSULTING SA)" Date: Thu, 18 Jun 2026 09:48:44 -0300 Subject: [PATCH 3/5] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- pipes-and-filters/ImageProcessingPipeline/Resize.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pipes-and-filters/ImageProcessingPipeline/Resize.cs b/pipes-and-filters/ImageProcessingPipeline/Resize.cs index 06aecefb..709490d8 100644 --- a/pipes-and-filters/ImageProcessingPipeline/Resize.cs +++ b/pipes-and-filters/ImageProcessingPipeline/Resize.cs @@ -32,9 +32,9 @@ public async Task RunAsync( using var original = SKBitmap.Decode(data); // Validate image decode succeeded - if (original.Width <= 0 || original.Height <= 0) + if (original is null || original.Width <= 0 || original.Height <= 0) { - _logger.LogError("Failed to decode image {filePath}: invalid dimensions {width}x{height}", imageFilePath, original.Width, original.Height); + _logger.LogError("Failed to decode image {filePath}: invalid or unsupported image", imageFilePath); throw new InvalidOperationException($"Image decode failed or image is empty: {imageFilePath}"); } From 0d3cdc5183c7a9eed03c05af8b4acf48d34f9912 Mon Sep 17 00:00:00 2001 From: Federico Arambarri Date: Thu, 18 Jun 2026 09:56:06 -0300 Subject: [PATCH 4/5] Addressing Copilot PR Comments --- pipes-and-filters/ImageProcessingPipeline/Resize.cs | 6 ++++++ .../ImageProcessingPipeline/Watermark.cs | 12 ++++++++++++ 2 files changed, 18 insertions(+) diff --git a/pipes-and-filters/ImageProcessingPipeline/Resize.cs b/pipes-and-filters/ImageProcessingPipeline/Resize.cs index 709490d8..50eeaede 100644 --- a/pipes-and-filters/ImageProcessingPipeline/Resize.cs +++ b/pipes-and-filters/ImageProcessingPipeline/Resize.cs @@ -31,6 +31,12 @@ public async Task RunAsync( using var data = SKData.Create(memoryStream); using var original = SKBitmap.Decode(data); + if (original is null) + { + _logger.LogError("Failed to decode image {filePath}: decode returned null.", imageFilePath); + throw new InvalidOperationException($"Image decode failed: {imageFilePath}"); + } + // Validate image decode succeeded if (original is null || original.Width <= 0 || original.Height <= 0) { diff --git a/pipes-and-filters/ImageProcessingPipeline/Watermark.cs b/pipes-and-filters/ImageProcessingPipeline/Watermark.cs index db9cb578..d4cc97c9 100644 --- a/pipes-and-filters/ImageProcessingPipeline/Watermark.cs +++ b/pipes-and-filters/ImageProcessingPipeline/Watermark.cs @@ -31,6 +31,12 @@ public async Task RunAsync( using var data = SKData.Create(memoryStream); using var original = SKBitmap.Decode(data); + if (original is null) + { + _logger.LogError("Failed to decode image {filePath}: decode returned null.", imageFilePath); + throw new InvalidOperationException($"Image decode failed: {imageFilePath}"); + } + // Validate image decode succeeded if (original.Width <= 0 || original.Height <= 0) { @@ -48,6 +54,12 @@ public async Task RunAsync( using var watermarkBitmap = SKBitmap.Decode(watermarkPath); + if (watermarkBitmap is null) + { + _logger.LogError("Failed to decode watermark image at {path}: decode returned null.", watermarkPath); + throw new InvalidOperationException($"Watermark decode failed: {watermarkPath}"); + } + // Validate watermark fits in image if (watermarkBitmap.Width > original.Width || watermarkBitmap.Height > original.Height) { From f243a6201080c56402fa9eda76f8af6c9ef486c7 Mon Sep 17 00:00:00 2001 From: Federico Arambarri Date: Thu, 18 Jun 2026 13:56:18 -0300 Subject: [PATCH 5/5] Addressing PR Comment --- pipes-and-filters/ImageProcessingPipeline/Resize.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pipes-and-filters/ImageProcessingPipeline/Resize.cs b/pipes-and-filters/ImageProcessingPipeline/Resize.cs index 50eeaede..7d31262e 100644 --- a/pipes-and-filters/ImageProcessingPipeline/Resize.cs +++ b/pipes-and-filters/ImageProcessingPipeline/Resize.cs @@ -38,9 +38,9 @@ public async Task RunAsync( } // Validate image decode succeeded - if (original is null || original.Width <= 0 || original.Height <= 0) + if (original.Width <= 0 || original.Height <= 0) { - _logger.LogError("Failed to decode image {filePath}: invalid or unsupported image", imageFilePath); + _logger.LogError("Failed to decode image {filePath}: invalid dimensions {width}x{height}", imageFilePath, original.Width, original.Height); throw new InvalidOperationException($"Image decode failed or image is empty: {imageFilePath}"); }