diff --git a/pipes-and-filters/ImageProcessingPipeline/ImageProcessingPipeline.csproj b/pipes-and-filters/ImageProcessingPipeline/ImageProcessingPipeline.csproj
index da9db8cd..02b4fbf3 100644
--- a/pipes-and-filters/ImageProcessingPipeline/ImageProcessingPipeline.csproj
+++ b/pipes-and-filters/ImageProcessingPipeline/ImageProcessingPipeline.csproj
@@ -5,7 +5,6 @@
Exe
enable
enable
- true
@@ -13,8 +12,8 @@
-
-
+
+
@@ -29,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..7d31262e 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,61 @@ 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);
+
+ 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)
+ {
+ _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 8afbe2c0..d4cc97c9 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,28 +20,82 @@ 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 resources = _files.GetDirectoryContents("/");
- using var watermarkStream = resources.First(resource => resource.Name.Equals("resources/watermark.png")).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);
- });
+ 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)
+ {
+ _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);
+
+ if (watermarkBitmap is null)
+ {
+ _logger.LogError("Failed to decode watermark image at {path}: decode returned null.", watermarkPath);
+ throw new InvalidOperationException($"Watermark decode failed: {watermarkPath}");
+ }
- // 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))
+ // 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);
+ }
+
+ // 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/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: {