From d62342aae1ee4bc619030e00203df018164efb0e Mon Sep 17 00:00:00 2001 From: MaddoScientisto Date: Sun, 8 Mar 2026 11:17:47 +0100 Subject: [PATCH] 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. +