From d62342aae1ee4bc619030e00203df018164efb0e Mon Sep 17 00:00:00 2001 From: MaddoScientisto Date: Sun, 8 Mar 2026 11:17:47 +0100 Subject: [PATCH 1/9] Implement ImageCreatorImageSharp using SixLabors.ImageSharp for image processing - Added ImageCreatorImageSharp class for image creation, handling EXIF orientation, resizing, and saving images. - Replaced GDI+ dependencies with ImageSharp for cross-platform compatibility. - Introduced methods for drawing text and logos on images, including handling transparency and positioning. - Created a test plan for validating ImageCreatorImageSharp functionality, focusing on image resizing, text positioning, logo features, and EXIF orientation. - Added documentation for the test plan outlining goals, project structure, and implementation notes. --- Catalog.sln | 16 +- .../Helpers/CreatorFactory.cs | 44 ++++++ .../Helpers/PixelInspector.cs | 40 +++++ .../Helpers/TempWorkspace.cs | 33 ++++ .../Helpers/TestImageFactory.cs | 45 ++++++ .../MaddoShared.ImageSharpTests.csproj | 27 ++++ .../Tests/ImageResizingTests.cs | 43 ++++++ .../Tests/TextPositioningTests.cs | 50 ++++++ ...mageCreatorSharp.cs => ImageCreatorGDI.cs} | 146 +++++++++--------- ...Alternate.cs => ImageCreatorImageSharp.cs} | 0 docs/image-generation-tests-plan.md | 85 ++++++++++ 11 files changed, 455 insertions(+), 74 deletions(-) create mode 100644 MaddoShared.ImageSharpTests/Helpers/CreatorFactory.cs create mode 100644 MaddoShared.ImageSharpTests/Helpers/PixelInspector.cs create mode 100644 MaddoShared.ImageSharpTests/Helpers/TempWorkspace.cs create mode 100644 MaddoShared.ImageSharpTests/Helpers/TestImageFactory.cs create mode 100644 MaddoShared.ImageSharpTests/MaddoShared.ImageSharpTests.csproj create mode 100644 MaddoShared.ImageSharpTests/Tests/ImageResizingTests.cs create mode 100644 MaddoShared.ImageSharpTests/Tests/TextPositioningTests.cs rename MaddoShared/{ImageCreatorSharp.cs => ImageCreatorGDI.cs} (91%) rename MaddoShared/{ImageCreatorAlternate.cs => ImageCreatorImageSharp.cs} (100%) create mode 100644 docs/image-generation-tests-plan.md diff --git a/Catalog.sln b/Catalog.sln index b92613e..5d1d09f 100644 --- a/Catalog.sln +++ b/Catalog.sln @@ -17,6 +17,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MaddoShared.Benchmarks", "M EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Catalog.Communication", "Catalog.Communication\Catalog.Communication.csproj", "{EF5D3B7E-F380-4976-A0A9-085FEA157F79}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MaddoShared.ImageSharpTests", "MaddoShared.ImageSharpTests\MaddoShared.ImageSharpTests.csproj", "{1528903F-3BF9-599C-2DD0-0AF7B5706675}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -87,14 +89,26 @@ Global {EF5D3B7E-F380-4976-A0A9-085FEA157F79}.Release|x64.Build.0 = Release|Any CPU {EF5D3B7E-F380-4976-A0A9-085FEA157F79}.Release|x86.ActiveCfg = Release|Any CPU {EF5D3B7E-F380-4976-A0A9-085FEA157F79}.Release|x86.Build.0 = Release|Any CPU + {1528903F-3BF9-599C-2DD0-0AF7B5706675}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1528903F-3BF9-599C-2DD0-0AF7B5706675}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1528903F-3BF9-599C-2DD0-0AF7B5706675}.Debug|x64.ActiveCfg = Debug|Any CPU + {1528903F-3BF9-599C-2DD0-0AF7B5706675}.Debug|x64.Build.0 = Debug|Any CPU + {1528903F-3BF9-599C-2DD0-0AF7B5706675}.Debug|x86.ActiveCfg = Debug|Any CPU + {1528903F-3BF9-599C-2DD0-0AF7B5706675}.Debug|x86.Build.0 = Debug|Any CPU + {1528903F-3BF9-599C-2DD0-0AF7B5706675}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1528903F-3BF9-599C-2DD0-0AF7B5706675}.Release|Any CPU.Build.0 = Release|Any CPU + {1528903F-3BF9-599C-2DD0-0AF7B5706675}.Release|x64.ActiveCfg = Release|Any CPU + {1528903F-3BF9-599C-2DD0-0AF7B5706675}.Release|x64.Build.0 = Release|Any CPU + {1528903F-3BF9-599C-2DD0-0AF7B5706675}.Release|x86.ActiveCfg = Release|Any CPU + {1528903F-3BF9-599C-2DD0-0AF7B5706675}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {AEBFE9E3-277C-4A7B-8448-145D1B11998B} = {A3D50937-74F6-4DC8-8D89-B534B484C0F9} - {EF5D3B7E-F380-4976-A0A9-085FEA157F79} = {A3D50937-74F6-4DC8-8D89-B534B484C0F9} {59952BE8-20B4-4BF2-9367-705F41395265} = {5F0BEF23-B1EA-4100-A772-DC455D40B1C1} + {EF5D3B7E-F380-4976-A0A9-085FEA157F79} = {A3D50937-74F6-4DC8-8D89-B534B484C0F9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {0E3ABC63-8601-4DAC-AFEA-33F3E8E36757} diff --git a/MaddoShared.ImageSharpTests/Helpers/CreatorFactory.cs b/MaddoShared.ImageSharpTests/Helpers/CreatorFactory.cs new file mode 100644 index 0000000..cb25390 --- /dev/null +++ b/MaddoShared.ImageSharpTests/Helpers/CreatorFactory.cs @@ -0,0 +1,44 @@ +using System.IO; +using Microsoft.Extensions.Logging.Abstractions; +using SixLabors.ImageSharp.PixelFormats; + +namespace MaddoShared.ImageSharpTests.Helpers +{ + public static class CreatorFactory + { + public static MaddoShared.PicSettings CreateDefaultPicSettings() + { + return new MaddoShared.PicSettings + { + DimStandard = 48, + DimStandardMiniatura = 12, + LarghezzaSmall = 150, + AltezzaSmall = 150, + LarghezzaBig = 800, + AltezzaBig = 600, + Trasparenza = 0, + IlFont = "Arial", + Grassetto = false, + Posizione = "CENTRO", + Allineamento = "CENTRO", + Margine = 10, + MargVert = 10, + TestoMin = false, + AggNumTempMin = false, + CreaMiniature = false, + LogoAggiungi = false, + LogoAltezza = 100, + LogoLarghezza = 100, + LogoMargine = "0", + JpegQuality = 90, + JpegQualityMin = 75, + }; + } + + public static MaddoShared.ImageCreatorImageSharp CreateImageCreator(MaddoShared.PicSettings settings) + { + var logger = NullLogger.Instance; + return new MaddoShared.ImageCreatorImageSharp(settings, logger); + } + } +} diff --git a/MaddoShared.ImageSharpTests/Helpers/PixelInspector.cs b/MaddoShared.ImageSharpTests/Helpers/PixelInspector.cs new file mode 100644 index 0000000..5642ec7 --- /dev/null +++ b/MaddoShared.ImageSharpTests/Helpers/PixelInspector.cs @@ -0,0 +1,40 @@ +using System; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +namespace MaddoShared.ImageSharpTests.Helpers +{ + public static class PixelInspector + { + public static int CountNonBackgroundPixels(string path, int x, int y, int width, int height, Rgba32 background, int tolerance = 0) + { + using var img = SixLabors.ImageSharp.Image.Load(path); + var bx = Math.Max(0, x); + var by = Math.Max(0, y); + var bw = Math.Min(width, img.Width - bx); + var bh = Math.Min(height, img.Height - by); + if (bw <= 0 || bh <= 0) return 0; + + int count = 0; + img.ProcessPixelRows(accessor => + { + for (int yy = by; yy < by + bh; yy++) + { + var row = accessor.GetRowSpan(yy); + for (int xx = bx; xx < bx + bw; xx++) + { + var p = row[xx]; + if (!IsApproximatelyEqual(p, background, tolerance)) count++; + } + } + }); + + return count; + } + + private static bool IsApproximatelyEqual(Rgba32 a, Rgba32 b, int tol) + { + return Math.Abs(a.R - b.R) <= tol && Math.Abs(a.G - b.G) <= tol && Math.Abs(a.B - b.B) <= tol && Math.Abs(a.A - b.A) <= tol; + } + } +} diff --git a/MaddoShared.ImageSharpTests/Helpers/TempWorkspace.cs b/MaddoShared.ImageSharpTests/Helpers/TempWorkspace.cs new file mode 100644 index 0000000..fba6bd8 --- /dev/null +++ b/MaddoShared.ImageSharpTests/Helpers/TempWorkspace.cs @@ -0,0 +1,33 @@ +using System; +using System.IO; + +namespace MaddoShared.ImageSharpTests.Helpers +{ + public sealed class TempWorkspace : IDisposable + { + public DirectoryInfo Root { get; } + public DirectoryInfo SourceDir { get; } + public DirectoryInfo DestDir { get; } + + public TempWorkspace() + { + var root = Path.Combine(Path.GetTempPath(), "MaddoShared.ImageSharpTests", Guid.NewGuid().ToString("N")); + Root = Directory.CreateDirectory(root); + SourceDir = Directory.CreateDirectory(Path.Combine(Root.FullName, "Source")); + DestDir = Directory.CreateDirectory(Path.Combine(Root.FullName, "Dest")); + } + + public void Dispose() + { + try + { + if (Root.Exists) + Root.Delete(true); + } + catch + { + // best-effort cleanup + } + } + } +} diff --git a/MaddoShared.ImageSharpTests/Helpers/TestImageFactory.cs b/MaddoShared.ImageSharpTests/Helpers/TestImageFactory.cs new file mode 100644 index 0000000..4f15dd8 --- /dev/null +++ b/MaddoShared.ImageSharpTests/Helpers/TestImageFactory.cs @@ -0,0 +1,45 @@ +using System.IO; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; + +namespace MaddoShared.ImageSharpTests.Helpers +{ + public static class TestImageFactory + { + public static string CreateSolidJpeg(string directory, string fileName, int width, int height, Rgba32 color) + { + Directory.CreateDirectory(directory); + var path = Path.Combine(directory, fileName); + using var img = new Image(width, height, color); + var encoder = new JpegEncoder { Quality = 90 }; + img.Save(path, encoder); + return path; + } + + public static string CreateSolidPng(string directory, string fileName, int width, int height, Rgba32 color) + { + Directory.CreateDirectory(directory); + var path = Path.Combine(directory, fileName); + using var img = new Image(width, height, color); + img.SaveAsPng(path); + return path; + } + + public static string CreateJpegWithExifOrientation(string directory, string fileName, int width, int height, Rgba32 color, ushort orientation) + { + Directory.CreateDirectory(directory); + var path = Path.Combine(directory, fileName); + using var img = new Image(width, height, color); + // Add EXIF orientation + var profile = new ExifProfile(); + profile.SetValue(ExifTag.Orientation, orientation); + img.Metadata.ExifProfile = profile; + var encoder = new JpegEncoder { Quality = 90 }; + img.Save(path, encoder); + return path; + } + } +} diff --git a/MaddoShared.ImageSharpTests/MaddoShared.ImageSharpTests.csproj b/MaddoShared.ImageSharpTests/MaddoShared.ImageSharpTests.csproj new file mode 100644 index 0000000..0fc1d12 --- /dev/null +++ b/MaddoShared.ImageSharpTests/MaddoShared.ImageSharpTests.csproj @@ -0,0 +1,27 @@ + + + + net10.0 + latest + enable + enable + false + + + + + + + + + + + + + + + + + + + diff --git a/MaddoShared.ImageSharpTests/Tests/ImageResizingTests.cs b/MaddoShared.ImageSharpTests/Tests/ImageResizingTests.cs new file mode 100644 index 0000000..b4bb333 --- /dev/null +++ b/MaddoShared.ImageSharpTests/Tests/ImageResizingTests.cs @@ -0,0 +1,43 @@ +using System.IO; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SixLabors.ImageSharp.PixelFormats; +using MaddoShared.ImageSharpTests.Helpers; + +namespace MaddoShared.ImageSharpTests.Tests +{ + [TestClass] + public class ImageResizingTests + { + [TestMethod] + public async Task BigImageResizesRespectSettings() + { + using var ws = new TempWorkspace(); + + // create a large input image + var inputPath = TestImageFactory.CreateSolidJpeg(ws.SourceDir.FullName, "input.jpg", 1600, 1200, new Rgba32(200, 200, 200, 255)); + + var pic = CreatorFactory.CreateDefaultPicSettings(); + pic.LarghezzaBig = 800; + pic.AltezzaBig = 600; + pic.CreaMiniature = false; + + var svc = CreatorFactory.CreateImageCreator(pic); + + var state = new MaddoShared.ImageState + { + WorkFile = new FileInfo(inputPath), + DestDir = ws.DestDir, + SourceDir = ws.SourceDir + }; + + await svc.CreateImageAsync(state, null); + + var outPath = Path.Combine(ws.DestDir.FullName, state.NomeFileBig); + using var outImg = SixLabors.ImageSharp.Image.Load(outPath); + + Assert.AreEqual(800, outImg.Width); + Assert.AreEqual(600, outImg.Height); + } + } +} diff --git a/MaddoShared.ImageSharpTests/Tests/TextPositioningTests.cs b/MaddoShared.ImageSharpTests/Tests/TextPositioningTests.cs new file mode 100644 index 0000000..3724dde --- /dev/null +++ b/MaddoShared.ImageSharpTests/Tests/TextPositioningTests.cs @@ -0,0 +1,50 @@ +using System.IO; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SixLabors.ImageSharp.PixelFormats; +using MaddoShared.ImageSharpTests.Helpers; + +namespace MaddoShared.ImageSharpTests.Tests +{ + [TestClass] + public class TextPositioningTests + { + [TestMethod] + public async Task TextAtBottom_IncreasesNonBackgroundPixelCountInBottomBand() + { + using var ws = new TempWorkspace(); + + // create white background input + var inputPath = TestImageFactory.CreateSolidJpeg(ws.SourceDir.FullName, "input.jpg", 800, 600, new Rgba32(255, 255, 255, 255)); + + var pic = CreatorFactory.CreateDefaultPicSettings(); + pic.Posizione = "BASSO"; + pic.DimStandard = 48; // big text + pic.TestoFirmaStart = "SAMPLE TEXT"; + pic.CreaMiniature = false; + + var svc = CreatorFactory.CreateImageCreator(pic); + + var state = new MaddoShared.ImageState + { + WorkFile = new FileInfo(inputPath), + DestDir = ws.DestDir, + SourceDir = ws.SourceDir + }; + + await svc.CreateImageAsync(state, null); + + var outPath = Path.Combine(ws.DestDir.FullName, state.NomeFileBig); + + // bottom band (lower 25% of image) + var bottomY = (int)(600 * 0.75); + var bottomCount = PixelInspector.CountNonBackgroundPixels(outPath, 0, bottomY, 800, 600 - bottomY, new Rgba32(255, 255, 255, 255), tolerance: 10); + + // top band (upper 25%) + var topCount = PixelInspector.CountNonBackgroundPixels(outPath, 0, 0, 800, (int)(600 * 0.25), new Rgba32(255, 255, 255, 255), tolerance: 10); + + Assert.IsTrue(bottomCount > 50, $"Expected text pixels in bottom band, found {bottomCount}"); + Assert.IsTrue(bottomCount > topCount, "Expected more non-background pixels at bottom than top"); + } + } +} diff --git a/MaddoShared/ImageCreatorSharp.cs b/MaddoShared/ImageCreatorGDI.cs similarity index 91% rename from MaddoShared/ImageCreatorSharp.cs rename to MaddoShared/ImageCreatorGDI.cs index 0b28d9f..15fd6f6 100644 --- a/MaddoShared/ImageCreatorSharp.cs +++ b/MaddoShared/ImageCreatorGDI.cs @@ -228,23 +228,23 @@ public class ImageCreatorGDI(PicSettings picSettings, ILogger l } } - private void CreateThumbnails(Image sourceImage, ImageState imgState, Bitmap imgOutputBig, ImageFormat format) - { - // Only skip thumbnail generation when the global "create thumbnails" flag is false. - // Whether thumbnails include text is handled by ShouldRenderText/CreateThumbnailWithText - if (!picSettings.CreaMiniature) - return; + private void CreateThumbnails(Image sourceImage, ImageState imgState, Bitmap imgOutputBig, ImageFormat format) + { + // Only skip thumbnail generation when the global "create thumbnails" flag is false. + // Whether thumbnails include text is handled by ShouldRenderText/CreateThumbnailWithText + if (!picSettings.CreaMiniature) + return; - PrepareSignatureText(imgState); + PrepareSignatureText(imgState); - if (IsSameDirectory(picSettings.DirectorySorgente, picSettings.DirectoryDestinazione)) - UpdateFilenameWithCode(imgState); + if (IsSameDirectory(picSettings.DirectorySorgente, picSettings.DirectoryDestinazione)) + UpdateFilenameWithCode(imgState); - if (ShouldRenderText()) - CreateThumbnailWithText(sourceImage, imgState, imgOutputBig, format); - else - CreateSimpleThumbnail(sourceImage, imgState, format); - } + if (ShouldRenderText()) + CreateThumbnailWithText(sourceImage, imgState, imgOutputBig, format); + else + CreateSimpleThumbnail(sourceImage, imgState, format); + } private void PrepareSignatureText(ImageState imgState) { @@ -294,7 +294,7 @@ public class ImageCreatorGDI(PicSettings picSettings, ILogger l // This leaves room for margins and prevents clipping int tempFontSize = imgState.DimensioneStandardMiniatura; float maxTextHeight = image.Height * 0.15f; - + while ((textSize.Width > image.Width * 0.95f || textSize.Height > maxTextHeight) && tempFontSize > 5) { tempFontSize = (tempFontSize > 20) ? tempFontSize - 5 : tempFontSize - 1; @@ -375,20 +375,20 @@ public class ImageCreatorGDI(PicSettings picSettings, ILogger l switch (picSettings.Posizione.ToUpper()) { case "ALTO": - { - imgState.YPosFromBottom = picSettings.Margine; - imgState.YPosFromBottom3 = picSettings.MargVert; - break; - } + { + imgState.YPosFromBottom = picSettings.Margine; + imgState.YPosFromBottom3 = picSettings.MargVert; + break; + } case "BASSO": - { - imgState.YPosFromBottom = - Convert.ToSingle((g.Height - crSize.Height - (g.Height * picSettings.Margine / 100.0))); - imgState.YPosFromBottom3 = - Convert.ToSingle((g.Height - crSize.Height - (g.Height * picSettings.MargVert / 100.0))); - break; - } + { + imgState.YPosFromBottom = + Convert.ToSingle((g.Height - crSize.Height - (g.Height * picSettings.Margine / 100.0))); + imgState.YPosFromBottom3 = + Convert.ToSingle((g.Height - crSize.Height - (g.Height * picSettings.MargVert / 100.0))); + break; + } } float xCenterOfImg = 0; @@ -396,27 +396,27 @@ public class ImageCreatorGDI(PicSettings picSettings, ILogger l switch (picSettings.Allineamento.ToUpper()) { case "SINISTRA": - { - xCenterOfImg = Convert.ToSingle((picSettings.Margine + (larghezzaStandard / (double)2))); - if ((larghezzaStandard / (double)2) > (g.Width / (double)2) - picSettings.Margine) - xCenterOfImg = Convert.ToSingle((g.Width / (double)2)); - break; - } + { + xCenterOfImg = Convert.ToSingle((picSettings.Margine + (larghezzaStandard / (double)2))); + if ((larghezzaStandard / (double)2) > (g.Width / (double)2) - picSettings.Margine) + xCenterOfImg = Convert.ToSingle((g.Width / (double)2)); + break; + } case "CENTRO": - { - xCenterOfImg = Convert.ToSingle((g.Width / (double)2)); - break; - } + { + xCenterOfImg = Convert.ToSingle((g.Width / (double)2)); + break; + } case "DESTRA": - { - xCenterOfImg = - Convert.ToSingle((g.Width - picSettings.Margine - (larghezzaStandard / (double)2))); - if ((larghezzaStandard / (double)2) > (g.Width / (double)2) - picSettings.Margine) - xCenterOfImg = Convert.ToSingle((g.Width / (double)2)); - break; - } + { + xCenterOfImg = + Convert.ToSingle((g.Width - picSettings.Margine - (larghezzaStandard / (double)2))); + if ((larghezzaStandard / (double)2) > (g.Width / (double)2) - picSettings.Margine) + xCenterOfImg = Convert.ToSingle((g.Width / (double)2)); + break; + } } strFormat.Alignment = StringAlignment.Center; @@ -528,7 +528,7 @@ public class ImageCreatorGDI(PicSettings picSettings, ILogger l { logoTransparencyValue = 100; } - + var colorMatrixElements = new[] { new[] { 1.0F, 0.0F, 0.0F, 0.0F, 0.0F }, new[] { 0.0F, 1.0F, 0.0F, 0.0F, 0.0F }, @@ -571,44 +571,44 @@ public class ImageCreatorGDI(PicSettings picSettings, ILogger l { case "SINISTRA": case "NESSUNA": - { - xPosOfWm = margineUsato; - break; - } + { + xPosOfWm = margineUsato; + break; + } case "CENTRO": - { - xPosOfWm = System.Convert.ToInt32((imgOutputBig.Width - nuovaSize.Width) / (double)2); - break; - } + { + xPosOfWm = System.Convert.ToInt32((imgOutputBig.Width - nuovaSize.Width) / (double)2); + break; + } case "DESTRA": - { - xPosOfWm = ((imgOutputBig.Width - nuovaSize.Width) - margineUsato); - break; - } + { + xPosOfWm = ((imgOutputBig.Width - nuovaSize.Width) - margineUsato); + break; + } } switch (logoV) { case "ALTO": case "NESSUNA": - { - yPosOfWm = margineUsato; - break; - } + { + yPosOfWm = margineUsato; + break; + } case "CENTRO": - { - yPosOfWm = System.Convert.ToInt32((imgOutputBig.Height - nuovaSize.Height) / (double)2); - break; - } + { + yPosOfWm = System.Convert.ToInt32((imgOutputBig.Height - nuovaSize.Height) / (double)2); + break; + } case "BASSO": - { - yPosOfWm = ((imgOutputBig.Height - nuovaSize.Height) - margineUsato); - break; - } + { + yPosOfWm = ((imgOutputBig.Height - nuovaSize.Height) - margineUsato); + break; + } } grWatermark.DrawImage(logo, new Rectangle(xPosOfWm, yPosOfWm, nuovaSize.Width, nuovaSize.Height), 0, 0, @@ -776,7 +776,7 @@ public class ImageCreatorGDI(PicSettings picSettings, ILogger l { // Use 1% of image height as minimum margin, or 10px, whichever is larger float minMargin = Math.Max(10f, imgHeight * 0.01f); - + switch (picSettings.Posizione.ToUpper()) { case "ALTO": @@ -787,18 +787,18 @@ public class ImageCreatorGDI(PicSettings picSettings, ILogger l case "BASSO": var bottomMargin1 = (float)(imgHeight * picSettings.Margine / 100.0); var bottomMargin4 = (float)(imgHeight * picSettings.MargVert / 100.0); - + // Position from bottom: bottom edge of text at desired margin from bottom // Y = imageHeight - textHeight - bottomMargin var desiredY1 = imgHeight - textHeight - bottomMargin1; var desiredY4 = imgHeight - textHeight - bottomMargin4; - + // Ensure text stays completely within bounds: // - Top edge must be >= minMargin (not clipped at top) // - Bottom edge must be <= imgHeight - minMargin (not clipped at bottom) var maxAllowedY1 = imgHeight - textHeight - minMargin; // Maximum Y to keep bottom margin var maxAllowedY4 = imgHeight - textHeight - minMargin; - + imgState.YPosFromBottom1 = Math.Max(minMargin, Math.Min(desiredY1, maxAllowedY1)); imgState.YPosFromBottom4 = Math.Max(minMargin, Math.Min(desiredY4, maxAllowedY4)); break; diff --git a/MaddoShared/ImageCreatorAlternate.cs b/MaddoShared/ImageCreatorImageSharp.cs similarity index 100% rename from MaddoShared/ImageCreatorAlternate.cs rename to MaddoShared/ImageCreatorImageSharp.cs diff --git a/docs/image-generation-tests-plan.md b/docs/image-generation-tests-plan.md new file mode 100644 index 0000000..dcddcd8 --- /dev/null +++ b/docs/image-generation-tests-plan.md @@ -0,0 +1,85 @@ +# Image generation test plan — ImageSharp-only (multiplatform) + +Goal +----- +Create an automated, cross-platform test project that validates `ImageCreatorImageSharp` behavior by generating synthetic input images and pixel-inspecting outputs produced by the library. + +Decisions +--------- +- Test only `ImageCreatorImageSharp` (multiplatform). +- Test project targets `net10.0` (not Windows-only) and uses SixLabors.ImageSharp for both generation and verification; avoid `System.Drawing.Common` in tests. +- Inputs are programmatically generated images (no checked-in large binaries). +- Verification uses pixel inspection (sample regions, color averages, non-background pixel counts). + +Project +------- +- Name: `MaddoShared.ImageSharpTests` (folder: `MaddoShared.ImageSharpTests/`) +- TargetFramework: `net10.0` (no Windows-only flags) +- Package references: + - `MSTest.TestFramework` / `MSTest.TestAdapter` / `Microsoft.NET.Test.Sdk` + - `FluentAssertions` + - `Moq` (if needed for loggers) + - `SixLabors.ImageSharp` and `SixLabors.ImageSharp.Drawing` (for image creation and pixel inspection) + - `Microsoft.Extensions.Logging.Abstractions` (lightweight logging) +- ProjectReference: `../MaddoShared/MaddoShared.csproj` + +Helpers (tests) +---------------- +- `TempWorkspace` — creates temporary `Source` and `Dest` folders, cleans up on dispose. +- `TestImageFactory` — creates synthetic JPEG/PNG inputs and in-memory logo PNG bytes. Also can write EXIF orientation values. +- `PixelInspector` — loads output using ImageSharp and exposes: + - `CountNonBackgroundPixels(path, Rectangle region, Rgba32 background, int tolerance)` + - `SampleAverageColor(path, Rectangle region)` + - Region helpers: top/bottom/left/right/center/quadrant rectangles for given image sizes +- `CreatorFactory` — builds `PicSettings` defaults and `ImageCreatorImageSharp` instances. + +Test cases (high-level) +----------------------- +1. Image resizing: verify big and small output dimensions respect settings. +2. Text positioning: for `Posizione` = ALTO/CENTRO/BASSO and `Allineamento` = SINISTRA/CENTRO/DESTRA assert text pixels appear in expected regions. +3. Text content: baseline (empty) vs non-empty comparisons to ensure text changes output; EXIF-vertical photo selects `TestoFirmaV`. +4. Logo positioning: use a solid-color logo (e.g., pure red PNG) and verify red pixels appear in the expected quadrant for combinations of `LogoPosizioneH` × `LogoPosizioneV` and with margins (absolute and percentage). +5. Logo features: opacity (logo color blending) and color-key transparency. +6. EXIF orientation: ensure rotation is applied and output contains no EXIF orientation tag. + +Verification approach +--------------------- +- For text: compare non-background pixel count in the target band vs opposite band (thresholded) to avoid brittle exact glyph placement checks. +- For logo: sample the quadrant where the logo should be; count logo-colored pixels and assert above threshold. +- For opacity: sample average color in logo region and assert it is blended when opacity <100%. + +Scope boundaries +---------------- +- In scope: `ImageCreatorImageSharp` behavior: resize, EXIF rotation, text presence/position, logo position/opactiy, thumbnails. +- Out of scope: `ImageCreatorGDI` (excluded), OCR verification of exact text glyphs, font-subpixel metrics, performance testing. + +Implementation notes +-------------------- +- Keep tests deterministic by creating solid-color inputs and simple logos. +- Use modest image sizes (e.g., 800×600) so tests run fast. +- Carefully choose thresholds for pixel-count assertions to be robust across fonts and rendering differences. + +Run & validate +--------------- +- Build: `dotnet build MaddoShared.ImageSharpTests` +- Test: `dotnet test MaddoShared.ImageSharpTests` + +Files to add +------------ +- `MaddoShared.ImageSharpTests/MaddoShared.ImageSharpTests.csproj` +- `MaddoShared.ImageSharpTests/Helpers/TempWorkspace.cs` +- `MaddoShared.ImageSharpTests/Helpers/TestImageFactory.cs` +- `MaddoShared.ImageSharpTests/Helpers/PixelInspector.cs` +- `MaddoShared.ImageSharpTests/Helpers/CreatorFactory.cs` +- `MaddoShared.ImageSharpTests/Tests/ImageResizingTests.cs` +- `MaddoShared.ImageSharpTests/Tests/TextPositioningTests.cs` +- `MaddoShared.ImageSharpTests/Tests/LogoPositioningTests.cs` +- `MaddoShared.ImageSharpTests/Tests/ExifOrientationTests.cs` +- `docs/image-generation-tests-plan.md` (this file) + +Next steps +---------- +1. Create the `MaddoShared.ImageSharpTests` project and add the helper files. +2. Implement the first set of tests (resizing + a simple text presence test) to validate the testing harness. +3. Iterate thresholds and add remaining tests. + From e80b427fccc2c507168992c7789aa4b32855fd78 Mon Sep 17 00:00:00 2001 From: MaddoScientisto Date: Sun, 8 Mar 2026 12:09:58 +0100 Subject: [PATCH 2/9] feat: Update Avalonia UI with new tab icons and styles; upgrade AutoMapper and Avalonia packages --- .gitignore | 1 + imagecatalog/AvaloniaApp.axaml | 2 + imagecatalog/AvaloniaMainWindow.axaml | 113 +++++-- imagecatalog/ImageCatalog 2 - Backup.csproj | 59 ---- imagecatalog/ImageCatalog 2.csproj | 11 +- imagecatalog/ImageCatalog 2.vbproj | 347 -------------------- 6 files changed, 101 insertions(+), 432 deletions(-) delete mode 100644 imagecatalog/ImageCatalog 2 - Backup.csproj delete mode 100644 imagecatalog/ImageCatalog 2.vbproj diff --git a/.gitignore b/.gitignore index 432b9b3..9a1e321 100644 --- a/.gitignore +++ b/.gitignore @@ -255,3 +255,4 @@ paket-files/ # JetBrains Rider .idea/ *.sln.iml +.vscode/settings.json diff --git a/imagecatalog/AvaloniaApp.axaml b/imagecatalog/AvaloniaApp.axaml index 8ce9be1..e4a021d 100644 --- a/imagecatalog/AvaloniaApp.axaml +++ b/imagecatalog/AvaloniaApp.axaml @@ -3,6 +3,8 @@ x:Class="ImageCatalog_2.AvaloniaApp"> + + diff --git a/imagecatalog/AvaloniaMainWindow.axaml b/imagecatalog/AvaloniaMainWindow.axaml index 4517b63..e9f6ef2 100644 --- a/imagecatalog/AvaloniaMainWindow.axaml +++ b/imagecatalog/AvaloniaMainWindow.axaml @@ -1,11 +1,12 @@ + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:views="clr-namespace:ImageCatalog_2.AvaloniaViews" + xmlns:iconPacks="https://github.com/MahApps/IconPacks.Avalonia" + x:Class="ImageCatalog_2.AvaloniaMainWindow" + mc:Ignorable="d" + Title="Image Catalog - Avalonia" Height="540" Width="800"> @@ -16,54 +17,124 @@ - + + + + + + + - + + + + + + + - + + + + + + + - + + + + + + + - + + + + + + + - + + + + + + + - + + + + + + + - + + + + + + + - - + diff --git a/imagecatalog/ImageCatalog 2 - Backup.csproj b/imagecatalog/ImageCatalog 2 - Backup.csproj deleted file mode 100644 index c77aca9..0000000 --- a/imagecatalog/ImageCatalog 2 - Backup.csproj +++ /dev/null @@ -1,59 +0,0 @@ - - - WinExe - net8.0-windows - enable - enable - true - False - 3.0.0.0 - 3.0.0.0 - 3.0.0-alpha.63+Branch.develop.Sha.39a9baf5c618d8d79c75b89e2d5c4020939697f2 - 3.0.0-alpha0063 - - - true - - - embedded - - - embedded - - - - MyApplicationCodeGenerator - Application.Designer.cs - - - SettingsSingleFileGenerator - Settings.Designer.cs - - - - - - - - - - - - - - - all - - - All - - - - - True - True - Settings.settings - - - - \ No newline at end of file diff --git a/imagecatalog/ImageCatalog 2.csproj b/imagecatalog/ImageCatalog 2.csproj index 36ac938..be6394f 100644 --- a/imagecatalog/ImageCatalog 2.csproj +++ b/imagecatalog/ImageCatalog 2.csproj @@ -51,17 +51,18 @@ - + + - - - - + + + + all diff --git a/imagecatalog/ImageCatalog 2.vbproj b/imagecatalog/ImageCatalog 2.vbproj deleted file mode 100644 index b8cc359..0000000 --- a/imagecatalog/ImageCatalog 2.vbproj +++ /dev/null @@ -1,347 +0,0 @@ - - - - Local - 9.0.30729 - 2.0 - {8D3AA2B0-8F06-4A61-9CAD-B920EB1A8E9C} - Debug - AnyCPU - - - - - ImageCatalog - - - None - JScript - Grid - IE50 - false - WinExe - Binary - On - On - ImageCatalog - ImageCatalog.My.MyApplication - - - WindowsForms - 3.5 - - - v4.7.2 - true - My Project\app.manifest - - - SAK - SAK - SAK - SAK - - - http://localhost/ImageCatalog/ - true - Web - true - Foreground - 7 - Days - false - false - true - 0 - 1.8.0.%2a - false - true - - - bin\ - ImageCatalog.xml - 285212672 - - - - - true - true - true - false - false - false - false - 1 - 42016,42017,42018,42019,42032,42353,42354,42355 - full - AnyCPU - AllRules.ruleset - false - - - bin\ - ImageCatalog.xml - 285212672 - - - - - false - true - false - true - false - false - false - 1 - 42016,42017,42018,42019,42032,42353,42354,42355 - none - AnyCPU - AllRules.ruleset - false - - - true - true - true - bin\x64\Debug\ - 285212672 - ImageCatalog.xml - 1 - 42016,42017,42018,42019,42032,42353,42354,42355 - full - x64 - AllRules.ruleset - false - - - true - bin\x64\Release\ - 285212672 - ImageCatalog.xml - true - 1 - 42016,42017,42018,42019,42032,42353,42354,42355 - x64 - AllRules.ruleset - false - - - true - true - true - bin\x86\Debug\ - 285212672 - ImageCatalog.xml - 1 - 42016,42017,42018,42019,42032,42353,42354,42355 - full - x86 - bin\ImageCatalog.exe.CodeAnalysisLog.xml - true - GlobalSuppressions.vb - AllRules.ruleset - ;F:\Program Files (x86)\Microsoft Visual Studio 10.0\Team Tools\Static Analysis Tools\\Rule Sets - true - ;F:\Program Files (x86)\Microsoft Visual Studio 10.0\Team Tools\Static Analysis Tools\FxCop\\Rules - true - false - false - - - true - bin\x86\Release\ - 285212672 - ImageCatalog.xml - true - 1 - 42016,42017,42018,42019,42032,42353,42354,42355 - x86 - bin\ImageCatalog.exe.CodeAnalysisLog.xml - true - GlobalSuppressions.vb - AllRules.ruleset - ;F:\Program Files (x86)\Microsoft Visual Studio 10.0\Team Tools\Static Analysis Tools\\Rule Sets - true - ;F:\Program Files (x86)\Microsoft Visual Studio 10.0\Team Tools\Static Analysis Tools\FxCop\\Rules - true - false - - - - ..\packages\Ben.Demystifier.0.3.0\lib\net45\Ben.Demystifier.dll - - - - System - - - ..\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll - - - ..\packages\System.Collections.Immutable.5.0.0\lib\net461\System.Collections.Immutable.dll - - - 3.5 - True - - - System.Data - True - - - System.Drawing - True - - - ..\packages\System.Memory.4.5.4\lib\net461\System.Memory.dll - - - - ..\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll - - - ..\packages\System.Reflection.Metadata.5.0.0\lib\net461\System.Reflection.Metadata.dll - - - ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.3\lib\net461\System.Runtime.CompilerServices.Unsafe.dll - - - ..\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll - - - System.Windows.Forms - - - System.XML - - - - - - - - - - - - - - - Code - - - - - - Code - - - Form - - - - MainForm.vb - - - Form - - - - True - Application.myapp - - - True - True - Settings.settings - - - - - Code - - - Code - - - Form1.vb - Designer - - - MainForm.vb - - - MyApplicationCodeGenerator - Application.Designer.vb - - - - - - My - SettingsSingleFileGenerator - Settings.Designer.vb - - - - - - False - .NET Framework 3.5 SP1 Client Profile - false - - - False - .NET Framework 2.0 %28x86%29 - false - - - False - .NET Framework 3.0 %28x86%29 - false - - - False - .NET Framework 3.5 - true - - - False - .NET Framework 3.5 SP1 - false - - - False - Windows Installer 3.1 - true - - - - - - - - {44465926-240d-473f-90b8-786ba4384406} - CatalogVbLib - - - {aebfe9e3-277c-4a7b-8448-145d1b11998b} - MaddoShared - - - - - - - - - - - - - - - - \ No newline at end of file From b29cc95a1e58c93565620a88cfc28c985c891bb4 Mon Sep 17 00:00:00 2001 From: MaddoScientisto Date: Sun, 8 Mar 2026 13:44:09 +0100 Subject: [PATCH 3/9] feat: Enhance Avalonia UI with compact styles and improved layout for various views; adjust margins and paddings for a denser interface --- imagecatalog/AvaloniaApp.axaml | 46 +++++++++++++++++++ imagecatalog/AvaloniaMainWindow.axaml | 34 +++++++------- imagecatalog/AvaloniaViews/AiTabView.axaml | 24 +++++----- .../AvaloniaViews/FaceAiTabView.axaml | 22 ++++----- .../AvaloniaViews/GeneralTabView.axaml | 36 +++++++-------- imagecatalog/AvaloniaViews/LogoTabView.axaml | 26 +++++------ imagecatalog/AvaloniaViews/PhotoTabView.axaml | 18 ++++---- .../AvaloniaViews/RaceUploadTabView.axaml | 11 ++--- imagecatalog/AvaloniaViews/TextTabView.axaml | 34 +++++++------- .../AvaloniaViews/ThumbnailsTabView.axaml | 16 +++---- 10 files changed, 156 insertions(+), 111 deletions(-) diff --git a/imagecatalog/AvaloniaApp.axaml b/imagecatalog/AvaloniaApp.axaml index e4a021d..fe06efa 100644 --- a/imagecatalog/AvaloniaApp.axaml +++ b/imagecatalog/AvaloniaApp.axaml @@ -4,6 +4,52 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/imagecatalog/AvaloniaMainWindow.axaml b/imagecatalog/AvaloniaMainWindow.axaml index e9f6ef2..bd8498f 100644 --- a/imagecatalog/AvaloniaMainWindow.axaml +++ b/imagecatalog/AvaloniaMainWindow.axaml @@ -10,17 +10,17 @@ - + - + - + @@ -30,7 +30,7 @@ - + @@ -40,7 +40,7 @@ - + @@ -50,7 +50,7 @@ - + @@ -60,7 +60,7 @@ - + @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + @@ -90,7 +90,7 @@ - + @@ -99,24 +99,24 @@ - + - + @@ -124,20 +124,20 @@ Command="{Binding ProcessImagesCommand}" IsEnabled="{Binding UiEnabled}"> - + - + diff --git a/imagecatalog/AvaloniaViews/AiTabView.axaml b/imagecatalog/AvaloniaViews/AiTabView.axaml index e9652d4..a5f6971 100644 --- a/imagecatalog/AvaloniaViews/AiTabView.axaml +++ b/imagecatalog/AvaloniaViews/AiTabView.axaml @@ -3,33 +3,33 @@ xmlns:avaloniaDataGrid="clr-namespace:Avalonia.Controls;assembly=Avalonia.Controls.DataGrid" x:Class="ImageCatalog_2.AvaloniaViews.AiTabView"> - + - + - - + + -