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.
This commit is contained in:
parent
90fb03bf0c
commit
d62342aae1
11 changed files with 455 additions and 74 deletions
16
Catalog.sln
16
Catalog.sln
|
|
@ -17,6 +17,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MaddoShared.Benchmarks", "M
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Catalog.Communication", "Catalog.Communication\Catalog.Communication.csproj", "{EF5D3B7E-F380-4976-A0A9-085FEA157F79}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Catalog.Communication", "Catalog.Communication\Catalog.Communication.csproj", "{EF5D3B7E-F380-4976-A0A9-085FEA157F79}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MaddoShared.ImageSharpTests", "MaddoShared.ImageSharpTests\MaddoShared.ImageSharpTests.csproj", "{1528903F-3BF9-599C-2DD0-0AF7B5706675}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
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|x64.Build.0 = Release|Any CPU
|
||||||
{EF5D3B7E-F380-4976-A0A9-085FEA157F79}.Release|x86.ActiveCfg = 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
|
{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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(NestedProjects) = preSolution
|
GlobalSection(NestedProjects) = preSolution
|
||||||
{AEBFE9E3-277C-4A7B-8448-145D1B11998B} = {A3D50937-74F6-4DC8-8D89-B534B484C0F9}
|
{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}
|
{59952BE8-20B4-4BF2-9367-705F41395265} = {5F0BEF23-B1EA-4100-A772-DC455D40B1C1}
|
||||||
|
{EF5D3B7E-F380-4976-A0A9-085FEA157F79} = {A3D50937-74F6-4DC8-8D89-B534B484C0F9}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {0E3ABC63-8601-4DAC-AFEA-33F3E8E36757}
|
SolutionGuid = {0E3ABC63-8601-4DAC-AFEA-33F3E8E36757}
|
||||||
|
|
|
||||||
44
MaddoShared.ImageSharpTests/Helpers/CreatorFactory.cs
Normal file
44
MaddoShared.ImageSharpTests/Helpers/CreatorFactory.cs
Normal file
|
|
@ -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<MaddoShared.ImageCreatorImageSharp>.Instance;
|
||||||
|
return new MaddoShared.ImageCreatorImageSharp(settings, logger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
40
MaddoShared.ImageSharpTests/Helpers/PixelInspector.cs
Normal file
40
MaddoShared.ImageSharpTests/Helpers/PixelInspector.cs
Normal file
|
|
@ -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<Rgba32>(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
MaddoShared.ImageSharpTests/Helpers/TempWorkspace.cs
Normal file
33
MaddoShared.ImageSharpTests/Helpers/TempWorkspace.cs
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
45
MaddoShared.ImageSharpTests/Helpers/TestImageFactory.cs
Normal file
45
MaddoShared.ImageSharpTests/Helpers/TestImageFactory.cs
Normal file
|
|
@ -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<Rgba32>(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<Rgba32>(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<Rgba32>(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
|
||||||
|
<PackageReference Include="MSTest.TestAdapter" Version="4.1.0" />
|
||||||
|
<PackageReference Include="MSTest.TestFramework" Version="4.1.0" />
|
||||||
|
<PackageReference Include="FluentAssertions" Version="8.8.0" />
|
||||||
|
<PackageReference Include="Moq" Version="4.20.72" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.3" />
|
||||||
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||||
|
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.7" />
|
||||||
|
<PackageReference Include="SixLabors.Fonts" Version="2.1.3" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\MaddoShared\MaddoShared.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
43
MaddoShared.ImageSharpTests/Tests/ImageResizingTests.cs
Normal file
43
MaddoShared.ImageSharpTests/Tests/ImageResizingTests.cs
Normal file
|
|
@ -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<Rgba32>(outPath);
|
||||||
|
|
||||||
|
Assert.AreEqual(800, outImg.Width);
|
||||||
|
Assert.AreEqual(600, outImg.Height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
50
MaddoShared.ImageSharpTests/Tests/TextPositioningTests.cs
Normal file
50
MaddoShared.ImageSharpTests/Tests/TextPositioningTests.cs
Normal file
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -228,23 +228,23 @@ public class ImageCreatorGDI(PicSettings picSettings, ILogger<ImageCreatorGDI> l
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CreateThumbnails(Image sourceImage, ImageState imgState, Bitmap imgOutputBig, ImageFormat format)
|
private void CreateThumbnails(Image sourceImage, ImageState imgState, Bitmap imgOutputBig, ImageFormat format)
|
||||||
{
|
{
|
||||||
// Only skip thumbnail generation when the global "create thumbnails" flag is false.
|
// Only skip thumbnail generation when the global "create thumbnails" flag is false.
|
||||||
// Whether thumbnails include text is handled by ShouldRenderText/CreateThumbnailWithText
|
// Whether thumbnails include text is handled by ShouldRenderText/CreateThumbnailWithText
|
||||||
if (!picSettings.CreaMiniature)
|
if (!picSettings.CreaMiniature)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
PrepareSignatureText(imgState);
|
PrepareSignatureText(imgState);
|
||||||
|
|
||||||
if (IsSameDirectory(picSettings.DirectorySorgente, picSettings.DirectoryDestinazione))
|
if (IsSameDirectory(picSettings.DirectorySorgente, picSettings.DirectoryDestinazione))
|
||||||
UpdateFilenameWithCode(imgState);
|
UpdateFilenameWithCode(imgState);
|
||||||
|
|
||||||
if (ShouldRenderText())
|
if (ShouldRenderText())
|
||||||
CreateThumbnailWithText(sourceImage, imgState, imgOutputBig, format);
|
CreateThumbnailWithText(sourceImage, imgState, imgOutputBig, format);
|
||||||
else
|
else
|
||||||
CreateSimpleThumbnail(sourceImage, imgState, format);
|
CreateSimpleThumbnail(sourceImage, imgState, format);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PrepareSignatureText(ImageState imgState)
|
private void PrepareSignatureText(ImageState imgState)
|
||||||
{
|
{
|
||||||
|
|
@ -294,7 +294,7 @@ public class ImageCreatorGDI(PicSettings picSettings, ILogger<ImageCreatorGDI> l
|
||||||
// This leaves room for margins and prevents clipping
|
// This leaves room for margins and prevents clipping
|
||||||
int tempFontSize = imgState.DimensioneStandardMiniatura;
|
int tempFontSize = imgState.DimensioneStandardMiniatura;
|
||||||
float maxTextHeight = image.Height * 0.15f;
|
float maxTextHeight = image.Height * 0.15f;
|
||||||
|
|
||||||
while ((textSize.Width > image.Width * 0.95f || textSize.Height > maxTextHeight) && tempFontSize > 5)
|
while ((textSize.Width > image.Width * 0.95f || textSize.Height > maxTextHeight) && tempFontSize > 5)
|
||||||
{
|
{
|
||||||
tempFontSize = (tempFontSize > 20) ? tempFontSize - 5 : tempFontSize - 1;
|
tempFontSize = (tempFontSize > 20) ? tempFontSize - 5 : tempFontSize - 1;
|
||||||
|
|
@ -375,20 +375,20 @@ public class ImageCreatorGDI(PicSettings picSettings, ILogger<ImageCreatorGDI> l
|
||||||
switch (picSettings.Posizione.ToUpper())
|
switch (picSettings.Posizione.ToUpper())
|
||||||
{
|
{
|
||||||
case "ALTO":
|
case "ALTO":
|
||||||
{
|
{
|
||||||
imgState.YPosFromBottom = picSettings.Margine;
|
imgState.YPosFromBottom = picSettings.Margine;
|
||||||
imgState.YPosFromBottom3 = picSettings.MargVert;
|
imgState.YPosFromBottom3 = picSettings.MargVert;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "BASSO":
|
case "BASSO":
|
||||||
{
|
{
|
||||||
imgState.YPosFromBottom =
|
imgState.YPosFromBottom =
|
||||||
Convert.ToSingle((g.Height - crSize.Height - (g.Height * picSettings.Margine / 100.0)));
|
Convert.ToSingle((g.Height - crSize.Height - (g.Height * picSettings.Margine / 100.0)));
|
||||||
imgState.YPosFromBottom3 =
|
imgState.YPosFromBottom3 =
|
||||||
Convert.ToSingle((g.Height - crSize.Height - (g.Height * picSettings.MargVert / 100.0)));
|
Convert.ToSingle((g.Height - crSize.Height - (g.Height * picSettings.MargVert / 100.0)));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
float xCenterOfImg = 0;
|
float xCenterOfImg = 0;
|
||||||
|
|
@ -396,27 +396,27 @@ public class ImageCreatorGDI(PicSettings picSettings, ILogger<ImageCreatorGDI> l
|
||||||
switch (picSettings.Allineamento.ToUpper())
|
switch (picSettings.Allineamento.ToUpper())
|
||||||
{
|
{
|
||||||
case "SINISTRA":
|
case "SINISTRA":
|
||||||
{
|
{
|
||||||
xCenterOfImg = Convert.ToSingle((picSettings.Margine + (larghezzaStandard / (double)2)));
|
xCenterOfImg = Convert.ToSingle((picSettings.Margine + (larghezzaStandard / (double)2)));
|
||||||
if ((larghezzaStandard / (double)2) > (g.Width / (double)2) - picSettings.Margine)
|
if ((larghezzaStandard / (double)2) > (g.Width / (double)2) - picSettings.Margine)
|
||||||
xCenterOfImg = Convert.ToSingle((g.Width / (double)2));
|
xCenterOfImg = Convert.ToSingle((g.Width / (double)2));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "CENTRO":
|
case "CENTRO":
|
||||||
{
|
{
|
||||||
xCenterOfImg = Convert.ToSingle((g.Width / (double)2));
|
xCenterOfImg = Convert.ToSingle((g.Width / (double)2));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "DESTRA":
|
case "DESTRA":
|
||||||
{
|
{
|
||||||
xCenterOfImg =
|
xCenterOfImg =
|
||||||
Convert.ToSingle((g.Width - picSettings.Margine - (larghezzaStandard / (double)2)));
|
Convert.ToSingle((g.Width - picSettings.Margine - (larghezzaStandard / (double)2)));
|
||||||
if ((larghezzaStandard / (double)2) > (g.Width / (double)2) - picSettings.Margine)
|
if ((larghezzaStandard / (double)2) > (g.Width / (double)2) - picSettings.Margine)
|
||||||
xCenterOfImg = Convert.ToSingle((g.Width / (double)2));
|
xCenterOfImg = Convert.ToSingle((g.Width / (double)2));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
strFormat.Alignment = StringAlignment.Center;
|
strFormat.Alignment = StringAlignment.Center;
|
||||||
|
|
@ -528,7 +528,7 @@ public class ImageCreatorGDI(PicSettings picSettings, ILogger<ImageCreatorGDI> l
|
||||||
{
|
{
|
||||||
logoTransparencyValue = 100;
|
logoTransparencyValue = 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
var colorMatrixElements = new[]
|
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 },
|
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<ImageCreatorGDI> l
|
||||||
{
|
{
|
||||||
case "SINISTRA":
|
case "SINISTRA":
|
||||||
case "NESSUNA":
|
case "NESSUNA":
|
||||||
{
|
{
|
||||||
xPosOfWm = margineUsato;
|
xPosOfWm = margineUsato;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "CENTRO":
|
case "CENTRO":
|
||||||
{
|
{
|
||||||
xPosOfWm = System.Convert.ToInt32((imgOutputBig.Width - nuovaSize.Width) / (double)2);
|
xPosOfWm = System.Convert.ToInt32((imgOutputBig.Width - nuovaSize.Width) / (double)2);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "DESTRA":
|
case "DESTRA":
|
||||||
{
|
{
|
||||||
xPosOfWm = ((imgOutputBig.Width - nuovaSize.Width) - margineUsato);
|
xPosOfWm = ((imgOutputBig.Width - nuovaSize.Width) - margineUsato);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (logoV)
|
switch (logoV)
|
||||||
{
|
{
|
||||||
case "ALTO":
|
case "ALTO":
|
||||||
case "NESSUNA":
|
case "NESSUNA":
|
||||||
{
|
{
|
||||||
yPosOfWm = margineUsato;
|
yPosOfWm = margineUsato;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "CENTRO":
|
case "CENTRO":
|
||||||
{
|
{
|
||||||
yPosOfWm = System.Convert.ToInt32((imgOutputBig.Height - nuovaSize.Height) / (double)2);
|
yPosOfWm = System.Convert.ToInt32((imgOutputBig.Height - nuovaSize.Height) / (double)2);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "BASSO":
|
case "BASSO":
|
||||||
{
|
{
|
||||||
yPosOfWm = ((imgOutputBig.Height - nuovaSize.Height) - margineUsato);
|
yPosOfWm = ((imgOutputBig.Height - nuovaSize.Height) - margineUsato);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
grWatermark.DrawImage(logo, new Rectangle(xPosOfWm, yPosOfWm, nuovaSize.Width, nuovaSize.Height), 0, 0,
|
grWatermark.DrawImage(logo, new Rectangle(xPosOfWm, yPosOfWm, nuovaSize.Width, nuovaSize.Height), 0, 0,
|
||||||
|
|
@ -776,7 +776,7 @@ public class ImageCreatorGDI(PicSettings picSettings, ILogger<ImageCreatorGDI> l
|
||||||
{
|
{
|
||||||
// Use 1% of image height as minimum margin, or 10px, whichever is larger
|
// Use 1% of image height as minimum margin, or 10px, whichever is larger
|
||||||
float minMargin = Math.Max(10f, imgHeight * 0.01f);
|
float minMargin = Math.Max(10f, imgHeight * 0.01f);
|
||||||
|
|
||||||
switch (picSettings.Posizione.ToUpper())
|
switch (picSettings.Posizione.ToUpper())
|
||||||
{
|
{
|
||||||
case "ALTO":
|
case "ALTO":
|
||||||
|
|
@ -787,18 +787,18 @@ public class ImageCreatorGDI(PicSettings picSettings, ILogger<ImageCreatorGDI> l
|
||||||
case "BASSO":
|
case "BASSO":
|
||||||
var bottomMargin1 = (float)(imgHeight * picSettings.Margine / 100.0);
|
var bottomMargin1 = (float)(imgHeight * picSettings.Margine / 100.0);
|
||||||
var bottomMargin4 = (float)(imgHeight * picSettings.MargVert / 100.0);
|
var bottomMargin4 = (float)(imgHeight * picSettings.MargVert / 100.0);
|
||||||
|
|
||||||
// Position from bottom: bottom edge of text at desired margin from bottom
|
// Position from bottom: bottom edge of text at desired margin from bottom
|
||||||
// Y = imageHeight - textHeight - bottomMargin
|
// Y = imageHeight - textHeight - bottomMargin
|
||||||
var desiredY1 = imgHeight - textHeight - bottomMargin1;
|
var desiredY1 = imgHeight - textHeight - bottomMargin1;
|
||||||
var desiredY4 = imgHeight - textHeight - bottomMargin4;
|
var desiredY4 = imgHeight - textHeight - bottomMargin4;
|
||||||
|
|
||||||
// Ensure text stays completely within bounds:
|
// Ensure text stays completely within bounds:
|
||||||
// - Top edge must be >= minMargin (not clipped at top)
|
// - Top edge must be >= minMargin (not clipped at top)
|
||||||
// - Bottom edge must be <= imgHeight - minMargin (not clipped at bottom)
|
// - Bottom edge must be <= imgHeight - minMargin (not clipped at bottom)
|
||||||
var maxAllowedY1 = imgHeight - textHeight - minMargin; // Maximum Y to keep bottom margin
|
var maxAllowedY1 = imgHeight - textHeight - minMargin; // Maximum Y to keep bottom margin
|
||||||
var maxAllowedY4 = imgHeight - textHeight - minMargin;
|
var maxAllowedY4 = imgHeight - textHeight - minMargin;
|
||||||
|
|
||||||
imgState.YPosFromBottom1 = Math.Max(minMargin, Math.Min(desiredY1, maxAllowedY1));
|
imgState.YPosFromBottom1 = Math.Max(minMargin, Math.Min(desiredY1, maxAllowedY1));
|
||||||
imgState.YPosFromBottom4 = Math.Max(minMargin, Math.Min(desiredY4, maxAllowedY4));
|
imgState.YPosFromBottom4 = Math.Max(minMargin, Math.Min(desiredY4, maxAllowedY4));
|
||||||
break;
|
break;
|
||||||
85
docs/image-generation-tests-plan.md
Normal file
85
docs/image-generation-tests-plan.md
Normal file
|
|
@ -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.
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue