diff --git a/CI/scripts/Test-SystemAssemblyReferences.ps1 b/CI/scripts/Test-SystemAssemblyReferences.ps1
index afdecae..b269129 100644
--- a/CI/scripts/Test-SystemAssemblyReferences.ps1
+++ b/CI/scripts/Test-SystemAssemblyReferences.ps1
@@ -66,17 +66,30 @@ function Get-AssemblyVersion([string] $path) {
finally { $stream.Dispose() }
}
-# Lowest assembly version the given package version provides to .NET Framework / netstandard2.0
-# consumers. The lib folders can differ (System.Memory 4.6.3: net462 = 4.0.5.0, netstandard2.0 = 4.0.2.0),
-# so take the minimum to be safe for every consumer.
-function Get-PackageAssemblyVersion([string] $id, [string] $version, [string] $assemblyName) {
+# Assembly version the given package version provides to consumers of our $tfm lib folder.
+# The package lib folders can differ (System.Memory 4.6.3: net462 = 4.0.5.0, netstandard2.0 = 4.0.2.0).
+# - .NET Framework group: the app resolves the package's own .NET Framework lib folder (the highest
+# net4x folder not above our target, else netstandard2.0), so check against that one.
+# - netstandard2.0 group: it can be consumed from any .NET Framework or netstandard app, so take the
+# minimum across those lib folders to be safe for every consumer.
+function Get-PackageAssemblyVersion([string] $id, [string] $version, [string] $assemblyName, [string] $tfm) {
$packageDir = Join-Path $PackagesRoot (Join-Path $id.ToLowerInvariant() $version.ToLowerInvariant())
if (-not (Test-Path $packageDir)) {
throw "Package $id $version is not in '$PackagesRoot'. Restore the solution before running this check."
}
- $versions = foreach ($lib in 'net462', 'net461', 'net46', 'net45', 'netstandard2.0') {
- $dll = Join-Path $packageDir "lib/$lib/$assemblyName.dll"
- if (Test-Path $dll) { Get-AssemblyVersion $dll }
+ $candidates = 'net48', 'net472', 'net471', 'net47', 'net462', 'net461', 'net46', 'net45'
+ if ($tfm -match '^net4\d+$') {
+ $ours = [int]($tfm.Substring(3).PadRight(3, '0'))
+ $lib = @($candidates | Where-Object { [int]($_.Substring(3).PadRight(3, '0')) -le $ours }) + 'netstandard2.0' |
+ Where-Object { Test-Path (Join-Path $packageDir "lib/$_/$assemblyName.dll") } |
+ Select-Object -First 1
+ $versions = if ($lib) { Get-AssemblyVersion (Join-Path $packageDir "lib/$lib/$assemblyName.dll") }
+ }
+ else {
+ $versions = foreach ($lib in $candidates + 'netstandard2.0') {
+ $dll = Join-Path $packageDir "lib/$lib/$assemblyName.dll"
+ if (Test-Path $dll) { Get-AssemblyVersion $dll }
+ }
}
if (-not $versions) {
throw "No .NET Framework / netstandard2.0 assembly found for $id $version in '$packageDir'."
@@ -129,7 +142,7 @@ foreach ($group in $nuspec.SelectNodes("//*[local-name()='dependencies']/*[local
continue
}
$packageVersion = $declared[$ref.Name]
- $provided = Get-PackageAssemblyVersion $ref.Name $packageVersion $ref.Name
+ $provided = Get-PackageAssemblyVersion $ref.Name $packageVersion $ref.Name $tfm
$status = if ($ref.Version -gt $provided) { 'FAIL' } else { 'ok' }
Write-Host ("[{0}] {1}: {2} {3} -> declared package {4} provides {5} {6}" -f $tfm, $dll.Name, $ref.Name, $ref.Version, $packageVersion, $provided, $status)
if ($status -eq 'FAIL') {
diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/IronSoftware.Drawing.Common.Tests.csproj b/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/IronSoftware.Drawing.Common.Tests.csproj
index 3023262..042025f 100644
--- a/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/IronSoftware.Drawing.Common.Tests.csproj
+++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/IronSoftware.Drawing.Common.Tests.csproj
@@ -14,8 +14,6 @@
-
-
@@ -28,6 +26,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/AnyBitmapFunctionality.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/AnyBitmapFunctionality.cs
index 1df011b..2704122 100644
--- a/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/AnyBitmapFunctionality.cs
+++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/AnyBitmapFunctionality.cs
@@ -250,6 +250,7 @@ public void AnyBitmap_should_get_Bytes()
Assert.Equal(expected, resultExport);
}
+#if !NET48
[FactWithAutomaticDisplayName]
public void AnyBitmap_should_set_Pixel()
{
@@ -290,6 +291,7 @@ public void AnyBitmap_should_set_Pixel()
#endif
}
}
+#endif
[FactWithAutomaticDisplayName]
public void AnyBitmap_should_get_Stream()
@@ -339,6 +341,7 @@ public void AnyBitmap_should_ToString()
Assert.Equal(expected, result);
}
+#if !NET48
[FactWithAutomaticDisplayName]
public void AnyBitmap_GetPixel_should_not_throw_OutOfMemoryException()
{
@@ -390,6 +393,7 @@ public void Clone_AnyBitmap()
AssertImageAreEqual("expected.png", "result.png", true);
}
+#endif
[FactWithAutomaticDisplayName]
public void Clone_Crop_AnyBitmap()
@@ -473,6 +477,7 @@ public void CastSKImage_from_AnyBitmap()
AssertImageAreEqual("expected.png", "result.png", true);
}
+#if !NET48
[FactWithAutomaticDisplayName]
public void CastSixLabors_to_AnyBitmap()
{
@@ -533,6 +538,7 @@ public void CastSixLabors_from_AnyBitmap_Rgba32(string filename)
AssertImageAreEqual("expected.bmp", "result.bmp", true);
}
+#endif
[FactWithAutomaticDisplayName]
public void CastBitmap_to_AnyBitmap_using_FromBitmap()
@@ -711,6 +717,7 @@ public void Create_Multi_page_Tiff_Paths()
File.Delete(outputImagePath);
}
+#if !NET48
[FactWithAutomaticDisplayName]
public void CreateMultiFrameTiffStream_Preserves_Mixed_Orientation()
{
@@ -753,6 +760,7 @@ public void CreateMultiFrameTiffBytes_Preserves_Per_Page_Dimensions_And_Resoluti
.Should().BeApproximately(150d, 2d);
}
}
+#endif
[FactWithAutomaticDisplayName]
public void CreateMultiFrameTiffStream_Empty_Sequence_Throws()
@@ -761,6 +769,7 @@ public void CreateMultiFrameTiffStream_Empty_Sequence_Throws()
act.Should().Throw();
}
+#if !NET48
[FactWithAutomaticDisplayName]
public void CreateMultiFrameTiffBytes_PreservesResolution_FromPixelsPerMeterSource()
{
@@ -777,12 +786,18 @@ public void CreateMultiFrameTiffBytes_PreservesResolution_FromPixelsPerMeterSour
ToDotsPerInch(pages[0].XResolution, pages[0].ResolutionUnit)
.Should().BeApproximately(dpi, 2d);
}
+#endif
[FactWithAutomaticDisplayName]
public void CreateMultiFrameTiff_Preserves_Rgb24_Pixels()
{
string jpgPath = GetRelativeFilePath("mountainclimbers.jpg");
+#if NET48
+ // JPEG decoders differ by a few levels; compare against the same engine's decode of the source.
+ using var expected = AnyBitmap.FromFile(jpgPath);
+#else
using var expected = SixLabors.ImageSharp.Image.Load(jpgPath);
+#endif
using var result = AnyBitmap.CreateMultiFrameTiff(new List { jpgPath });
@@ -801,7 +816,11 @@ public void CreateMultiFrameTiff_Preserves_Rgb24_Pixels()
foreach (var (x, y) in points)
{
+#if NET48
+ var e = expected.GetPixel(x, y);
+#else
Rgb24 e = expected[x, y];
+#endif
var a = result.GetPixel(x, y);
a.R.Should().Be(e.R, $"red channel at ({x},{y})");
a.G.Should().Be(e.G, $"green channel at ({x},{y})");
@@ -809,6 +828,7 @@ public void CreateMultiFrameTiff_Preserves_Rgb24_Pixels()
}
}
+#if !NET48
[TheoryWithAutomaticDisplayName]
[InlineData("Rgb24")]
[InlineData("Bgr24")]
@@ -873,6 +893,7 @@ private static AnyBitmap MakeBitmapWithResolution(int width, int height, Rgb24 c
image.Metadata.ResolutionUnits = unit;
return image;
}
+#endif
private static List<(int Width, int Height, float XResolution, ResUnit ResolutionUnit)> ReadTiffDirectories(byte[] tiffData)
{
@@ -913,6 +934,7 @@ private static double ToDotsPerInch(float xResolution, ResUnit unit)
};
}
+#if !NET48
[FactWithAutomaticDisplayName]
public void Create_Multi_page_Gif()
{
@@ -988,6 +1010,7 @@ public void Should_Return_BitsPerPixel()
bitmap = Image.Load(GetRelativeFilePath("mountainclimbers.jpg"));
Assert.Equal(32, bitmap.BitsPerPixel);
}
+#endif
[TheoryWithAutomaticDisplayName]
[InlineData("ScanDev_BW.tif", 1)]
@@ -1173,6 +1196,7 @@ public void Resize_ShouldPreserveDepthForRepresentableFormats()
}
}
+#if !NET48
[TheoryWithAutomaticDisplayName]
[InlineData("A8", "GrayscaleWithAlpha", "Bit8", 16)]
[InlineData("L8", "Grayscale", "Bit8", 8)]
@@ -1219,6 +1243,7 @@ public void ExportPng_ShouldChooseColorTypeAndBitDepthFromPixelType(
}
Assert.Equal(expectedBitsPerPixel, AnyBitmap.FromBytes(png).BitsPerPixel);
}
+#endif
[TheoryWithAutomaticDisplayName]
[InlineData("Palette", "Bit8")]
@@ -1369,6 +1394,7 @@ public void Should_RotateFlip()
AssertImageAreEqual(GetRelativeFilePath("checkmarkFlip.jpg"), "result_flip.bmp");
}
+#if !NET48
[FactWithAutomaticDisplayName]
public void Redact_ShouldRedactRegionWithColor()
{
@@ -1409,6 +1435,7 @@ public void Redact_ShouldRedactRegionWithColor()
}
}
}
+#endif
[FactWithAutomaticDisplayName]
public void TestGetRGBBuffer()
@@ -1654,6 +1681,7 @@ public void LoadImage_SetPreserveOriginalFormat_ShouldReturnCorrectBitPerPixel(s
}
#if !NET7_0
+#if !NET48
[FactWithAutomaticDisplayName]
public void CastAnyBitmap_from_SixLabors()
{
@@ -1669,6 +1697,7 @@ public void CastAnyBitmap_from_SixLabors()
AssertLargeImageAreEqual("expected.bmp", "result.bmp", true);
}
#endif
+#endif
[IgnoreOnAzureDevopsX86Fact]
diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/ColorFunctionality.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/ColorFunctionality.cs
index 08bdb1d..79a5e8e 100644
--- a/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/ColorFunctionality.cs
+++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/ColorFunctionality.cs
@@ -274,6 +274,7 @@ public void Cast_SKColor_to_Color()
Assert.Equal(176, skColor.Blue);
}
+#if !NET48
[FactWithAutomaticDisplayName]
public void Cast_ImageSharp_Color_from_Color()
{
@@ -557,6 +558,7 @@ public void Should_Return_Argb()
Assert.Equal(bmColor.ToArgb(), rgba32.ToArgb());
Assert.Equal(bmColor.ToArgb(), rgb24.ToArgb());
}
+#endif
[FactWithAutomaticDisplayName]
public void Should_Create_FromName()
diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/Compare.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/Compare.cs
index bc4a4a2..58c13a7 100644
--- a/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/Compare.cs
+++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/Compare.cs
@@ -25,9 +25,23 @@ protected static void AssertLargeImageAreEqual(string expectedImagePath, string
expectedImageSharp.Mutate(x => x.Resize(100, 100));
actualImageSharp.Mutate(x => x.Resize(100, 100));
+#if NET48
+ // The ImageSharp casts are not available on .NET Framework; go through PNG bytes instead.
+ AssertImageAreEqual(ToAnyBitmap(expectedImageSharp), ToAnyBitmap(actualImageSharp));
+#else
AssertImageAreEqual(expectedImageSharp, actualImageSharp);
+#endif
}
+#if NET48
+ private static AnyBitmap ToAnyBitmap(SixLabors.ImageSharp.Image image)
+ {
+ using var stream = new MemoryStream();
+ SixLabors.ImageSharp.ImageExtensions.SaveAsPng(image, stream);
+ return AnyBitmap.FromBytes(stream.ToArray());
+ }
+#endif
+
protected static void AssertImageAreEqual(string expectedImagePath, string resultImagePath, bool isCleanAll = false)
{
using var expected = AnyBitmap.FromFile(expectedImagePath);
diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/RectangleFunctionality.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/RectangleFunctionality.cs
index a12e3ad..64edcb6 100644
--- a/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/RectangleFunctionality.cs
+++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/RectangleFunctionality.cs
@@ -70,6 +70,7 @@ public void CastRectangle_from_Rectangle()
Assert.Equal(5, rectangle.Y);
}
+#if !NET48
[FactWithAutomaticDisplayName]
public void CastImageSharp_Rectangle_to_Rectangle()
{
@@ -137,6 +138,7 @@ public void CastImageSharp_RectangleF_from_Rectangle()
Assert.Equal(5, rectangle.X);
Assert.Equal(5, rectangle.Y);
}
+#endif
[FactWithAutomaticDisplayName]
public void CastSKRect_to_Rectangle()
@@ -411,6 +413,7 @@ public void CastRectangleF_from_RectangleF()
Assert.Equal(5, Rectangle.Y);
}
+#if !NET48
[FactWithAutomaticDisplayName]
public void CastImageSharp_RectangleF_to_RectangleF()
{
@@ -443,6 +446,7 @@ public void CastImageSharp_RectangleF_from_RectangleF()
Assert.Equal(5, Rectangle.X);
Assert.Equal(5, Rectangle.Y);
}
+#endif
[FactWithAutomaticDisplayName]
public void CastSKRect_to_RectangleF()
diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/SkiaEngineFunctionality.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/SkiaEngineFunctionality.cs
new file mode 100644
index 0000000..f344966
--- /dev/null
+++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/SkiaEngineFunctionality.cs
@@ -0,0 +1,184 @@
+#if NET48
+using FluentAssertions;
+using System.Collections.Generic;
+using System.Linq;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace IronSoftware.Drawing.Common.Tests.UnitTests
+{
+ ///
+ /// .NET Framework runs on the SkiaSharp engine (no ImageSharp). These tests cover the behaviour that the
+ /// ImageSharp-cast tests check on other targets, using only the public AnyBitmap API.
+ ///
+ public class SkiaEngineFunctionality : Compare
+ {
+ public SkiaEngineFunctionality(ITestOutputHelper output) : base(output)
+ {
+ }
+
+ private static AnyBitmap MakeBitmap(int width, int height)
+ {
+ var bitmap = new AnyBitmap(width, height, Color.White);
+ for (int y = 0; y < height; y++)
+ {
+ for (int x = 0; x < width; x++)
+ {
+ bitmap.SetPixel(x, y, Color.FromArgb(255, x * 255 / width, y * 255 / height, 128));
+ }
+ }
+
+ return bitmap;
+ }
+
+ [FactWithAutomaticDisplayName]
+ public void SetPixel_GetPixel_RoundTrip()
+ {
+ using var bitmap = AnyBitmap.FromFile(GetRelativeFilePath("mountainclimbers.jpg"));
+ bitmap.GetPixel(0, 0).Should().NotBe(Color.Black);
+
+ bitmap.SetPixel(0, 0, Color.Black);
+ bitmap.SetPixel(5, 7, Color.FromArgb(255, 10, 20, 30));
+
+ bitmap.GetPixel(0, 0).Should().Be(Color.Black);
+ bitmap.GetPixel(5, 7).Should().Be(Color.FromArgb(255, 10, 20, 30));
+ }
+
+ [TheoryWithAutomaticDisplayName]
+ [InlineData(AnyBitmap.ImageFormat.Png)]
+ [InlineData(AnyBitmap.ImageFormat.Bmp)]
+ [InlineData(AnyBitmap.ImageFormat.Tiff)]
+ public void LosslessFormats_RoundTripPixels(AnyBitmap.ImageFormat format)
+ {
+ using var source = MakeBitmap(40, 30);
+ using var decoded = AnyBitmap.FromBytes(source.ExportBytes(format));
+
+ decoded.Width.Should().Be(40);
+ decoded.Height.Should().Be(30);
+ foreach (var (x, y) in new[] { (0, 0), (39, 0), (20, 15), (0, 29), (39, 29) })
+ {
+ decoded.GetPixel(x, y).Should().Be(source.GetPixel(x, y), $"{format} pixel ({x},{y})");
+ }
+ }
+
+ [TheoryWithAutomaticDisplayName]
+ [InlineData(AnyBitmap.ImageFormat.Jpeg, "image/jpeg")]
+ [InlineData(AnyBitmap.ImageFormat.Webp, "image/webp")]
+ [InlineData(AnyBitmap.ImageFormat.Gif, "image/gif")]
+ public void LossyFormats_RoundTripCloseToSource(AnyBitmap.ImageFormat format, string mimeType)
+ {
+ using var source = new AnyBitmap(32, 32, Color.FromArgb(255, 200, 40, 90));
+ using var decoded = AnyBitmap.FromBytes(source.ExportBytes(format, 90));
+
+ decoded.MimeType.Should().Be(mimeType);
+ Color pixel = decoded.GetPixel(16, 16);
+ ((int)pixel.R).Should().BeCloseTo(200, 6);
+ ((int)pixel.G).Should().BeCloseTo(40, 6);
+ ((int)pixel.B).Should().BeCloseTo(90, 6);
+ }
+
+ [FactWithAutomaticDisplayName]
+ public void Png_PreservesTransparency()
+ {
+ using var source = new AnyBitmap(10, 10, Color.Transparent);
+ source.SetPixel(3, 3, Color.FromArgb(128, 255, 0, 0));
+
+ using var decoded = AnyBitmap.FromBytes(source.ExportBytes(AnyBitmap.ImageFormat.Png));
+
+ decoded.GetPixel(0, 0).A.Should().Be(0);
+ decoded.GetPixel(3, 3).Should().Be(Color.FromArgb(128, 255, 0, 0));
+ }
+
+ [FactWithAutomaticDisplayName]
+ public void Clone_Rectangle_CropsPixels()
+ {
+ using var source = MakeBitmap(50, 40);
+ using var cropped = source.Clone(new Rectangle(10, 5, 20, 15));
+
+ cropped.Width.Should().Be(20);
+ cropped.Height.Should().Be(15);
+ cropped.GetPixel(0, 0).Should().Be(source.GetPixel(10, 5));
+ cropped.GetPixel(19, 14).Should().Be(source.GetPixel(29, 19));
+ }
+
+ [FactWithAutomaticDisplayName]
+ public void Redact_FillsOnlyTheRegion()
+ {
+ using var source = new AnyBitmap(100, 100, Color.White);
+ using var redacted = source.Redact(new Rectangle(10, 10, 30, 20), Color.Red);
+
+ redacted.GetPixel(10, 10).Should().Be(Color.Red);
+ redacted.GetPixel(39, 29).Should().Be(Color.Red);
+ redacted.GetPixel(9, 10).Should().Be(Color.White);
+ redacted.GetPixel(40, 30).Should().Be(Color.White);
+ }
+
+ [FactWithAutomaticDisplayName]
+ public void RotateFlip_MovesPixels()
+ {
+ using var source = MakeBitmap(20, 10);
+ using var rotated = source.RotateFlip(AnyBitmap.RotateMode.Rotate90, AnyBitmap.FlipMode.None);
+
+ rotated.Width.Should().Be(10);
+ rotated.Height.Should().Be(20);
+ // Rotating 90 degrees clockwise moves the top-left pixel to the top-right corner.
+ rotated.GetPixel(9, 0).Should().Be(source.GetPixel(0, 0));
+ }
+
+ [FactWithAutomaticDisplayName]
+ public void Resize_ChangesDimensions()
+ {
+ using var source = AnyBitmap.FromFile(GetRelativeFilePath("mountainclimbers.jpg"));
+ using var resized = new AnyBitmap(source, 64, 48);
+
+ resized.Width.Should().Be(64);
+ resized.Height.Should().Be(48);
+ }
+
+ [FactWithAutomaticDisplayName]
+ public void MultiFrameGif_KeepsEveryFrame()
+ {
+ var frames = new List
+ {
+ new AnyBitmap(30, 30, Color.Red),
+ new AnyBitmap(30, 30, Color.Lime),
+ new AnyBitmap(30, 30, Color.Blue)
+ };
+
+ using var gif = AnyBitmap.CreateMultiFrameGif(frames);
+ using var decoded = AnyBitmap.FromBytes(gif.ExportBytes(AnyBitmap.ImageFormat.Gif));
+
+ decoded.FrameCount.Should().Be(3);
+ var decodedFrames = decoded.GetAllFrames.ToList();
+ decodedFrames[0].GetPixel(15, 15).Should().Be(Color.Red);
+ decodedFrames[1].GetPixel(15, 15).Should().Be(Color.Lime);
+ decodedFrames[2].GetPixel(15, 15).Should().Be(Color.Blue);
+ }
+
+ [FactWithAutomaticDisplayName]
+ public void MultiFrameTiff_KeepsEveryPage()
+ {
+ var pages = new List { MakeBitmap(40, 30), MakeBitmap(40, 30) };
+ pages[1].SetPixel(0, 0, Color.Black);
+
+ using var tiff = AnyBitmap.CreateMultiFrameTiff(pages);
+ using var decoded = AnyBitmap.FromBytes(tiff.ExportBytes(AnyBitmap.ImageFormat.Tiff));
+
+ decoded.FrameCount.Should().Be(2);
+ var decodedPages = decoded.GetAllFrames.ToList();
+ decodedPages[0].GetPixel(0, 0).Should().Be(pages[0].GetPixel(0, 0));
+ decodedPages[1].GetPixel(0, 0).Should().Be(Color.Black);
+ }
+
+ [TheoryWithAutomaticDisplayName]
+ [InlineData("24_bit.png", 24)]
+ [InlineData("mountainclimbers.jpg", 24)]
+ [InlineData("32_bit_transparent.png", 32)]
+ public void BitsPerPixel_FollowsSource(string fileName, int expected)
+ {
+ using var bitmap = AnyBitmap.FromFile(GetRelativeFilePath(fileName), true);
+ bitmap.BitsPerPixel.Should().Be(expected);
+ }
+ }
+}
+#endif
diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/TiffDirectoryGuardFunctionality.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/TiffDirectoryGuardFunctionality.cs
index 2fa4c57..bf17dcc 100644
--- a/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/TiffDirectoryGuardFunctionality.cs
+++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/TiffDirectoryGuardFunctionality.cs
@@ -129,6 +129,7 @@ public void CreateMultiFrameTiff_BigTiffWithOversizedIfdCount_FailsFast()
}
}
+#if !NET48
[FactWithAutomaticDisplayName]
public void ImageSharpLoader_BigTiffWithOversizedIfdCount_FailsFast()
{
@@ -138,7 +139,9 @@ public void ImageSharpLoader_BigTiffWithOversizedIfdCount_FailsFast()
ShouldFailFast(() => ImageSharpLoader.Load(new MemoryStream(bytes)));
ShouldFailFast(() => ImageSharpLoader.Load(new MemoryStream(bytes)));
}
+#endif
+#if !NET48
[FactWithAutomaticDisplayName]
public void ImageSharpLoader_ValidImage_MatchesImageLoad()
{
@@ -154,6 +157,7 @@ public void ImageSharpLoader_ValidImage_MatchesImageLoad()
fromStream.Width.Should().Be(expected.Width);
fromStream.Height.Should().Be(expected.Height);
}
+#endif
[FactWithAutomaticDisplayName]
public void FromBytes_ValidBigTiff_StillLoads()
diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common/AnyBitmap.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common/AnyBitmap.cs
index bddea65..f2dfb92 100644
--- a/IronSoftware.Drawing/IronSoftware.Drawing.Common/AnyBitmap.cs
+++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common/AnyBitmap.cs
@@ -1,5 +1,12 @@
using BitMiracle.LibTiff.Classic;
using Microsoft.Maui.Graphics.Platform;
+#if NETFRAMEWORK
+// .NET Framework: SkiaSharp-based engine with the same API surface (ImageSharp 2.x is end-of-life).
+using IronSoftware.Drawing.SkiaEngine;
+using EngineRotateMode = IronSoftware.Drawing.SkiaEngine.RotateMode;
+using EngineFlipMode = IronSoftware.Drawing.SkiaEngine.FlipMode;
+using EnginePixelResolutionUnit = IronSoftware.Drawing.SkiaEngine.PixelResolutionUnit;
+#else
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.Formats;
@@ -12,6 +19,10 @@
using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
+using EngineRotateMode = SixLabors.ImageSharp.Processing.RotateMode;
+using EngineFlipMode = SixLabors.ImageSharp.Processing.FlipMode;
+using EnginePixelResolutionUnit = SixLabors.ImageSharp.Metadata.PixelResolutionUnit;
+#endif
using SkiaSharp;
using System;
using System.Collections.Generic;
@@ -1190,8 +1201,8 @@ public IEnumerable GetAllFrames
{
var images = GetInternalImages();
IEnumerable frames = images.Count == 1
- ? ImageFrameCollectionToImages(images[0].Frames).Select(x => (AnyBitmap)x)
- : images.Select(x => (AnyBitmap)x);
+ ? ImageFrameCollectionToImages(images[0].Frames).Select(x => new AnyBitmap(x))
+ : images.Select(x => new AnyBitmap(x));
// Each frame is a freshly-cast AnyBitmap. Carry the captured original source depth
// onto each frame. Only do this when this object captured an original depth (i.e. a
@@ -1243,7 +1254,7 @@ public static AnyBitmap CreateMultiFrameTiff(IEnumerable imagePaths)
public static AnyBitmap CreateMultiFrameTiff(IEnumerable images)
{
using var stream = new MemoryStream();
- InternalSaveAsMultiPageTiff(images.Select(x => (Image)x), stream);
+ InternalSaveAsMultiPageTiff(images.Select(CastToImageSharp), stream);
return FromStream(stream);
}
@@ -1273,7 +1284,7 @@ public static MemoryStream CreateMultiFrameTiffStream(IEnumerable ima
if (image == null)
throw new ArgumentException("The image sequence contains a null element.", nameof(images));
- frames.Add(((Image)image).CloneAs());
+ frames.Add(CastToImageSharp(image).CloneAs());
}
if (frames.Count == 0)
@@ -1337,7 +1348,7 @@ public static AnyBitmap CreateMultiFrameGif(IEnumerable imagePaths)
public static AnyBitmap CreateMultiFrameGif(IEnumerable images)
{
using var stream = new MemoryStream();
- InternalSaveAsMultiPageGif(images.Select(x => (Image)x), stream);
+ InternalSaveAsMultiPageGif(images.Select(CastToImageSharp), stream);
return FromStream(stream);
}
@@ -1470,20 +1481,20 @@ public static AnyBitmap RotateFlip(
RotateMode rotateMode,
FlipMode flipMode)
{
- SixLabors.ImageSharp.Processing.RotateMode rotateModeImgSharp = rotateMode switch
+ EngineRotateMode rotateModeImgSharp = rotateMode switch
{
- RotateMode.None => SixLabors.ImageSharp.Processing.RotateMode.None,
- RotateMode.Rotate90 => SixLabors.ImageSharp.Processing.RotateMode.Rotate90,
- RotateMode.Rotate180 => SixLabors.ImageSharp.Processing.RotateMode.Rotate180,
- RotateMode.Rotate270 => SixLabors.ImageSharp.Processing.RotateMode.Rotate270,
+ RotateMode.None => EngineRotateMode.None,
+ RotateMode.Rotate90 => EngineRotateMode.Rotate90,
+ RotateMode.Rotate180 => EngineRotateMode.Rotate180,
+ RotateMode.Rotate270 => EngineRotateMode.Rotate270,
_ => throw new NotImplementedException()
};
- SixLabors.ImageSharp.Processing.FlipMode flipModeImgSharp = flipMode switch
+ EngineFlipMode flipModeImgSharp = flipMode switch
{
- FlipMode.None => SixLabors.ImageSharp.Processing.FlipMode.None,
- FlipMode.Horizontal => SixLabors.ImageSharp.Processing.FlipMode.Horizontal,
- FlipMode.Vertical => SixLabors.ImageSharp.Processing.FlipMode.Vertical,
+ FlipMode.None => EngineFlipMode.None,
+ FlipMode.Horizontal => EngineFlipMode.Horizontal,
+ FlipMode.Vertical => EngineFlipMode.Vertical,
_ => throw new NotImplementedException()
};
@@ -2068,6 +2079,7 @@ public byte[] GetRGBABuffer()
}
#region Implicit Casting
+#if !NETFRAMEWORK
///
/// Implicitly casts SixLabors.ImageSharp.Image objects to
@@ -2099,6 +2111,7 @@ public static implicit operator AnyBitmap(Image image)
"Error while casting AnyBitmap from SixLabors.ImageSharp.Image", e);
}
}
+#endif
///
/// Since we store ImageSharp object internal AnyBitmap (lazy) so this casting will return the same ImageSharp object if it loaded.
@@ -2165,7 +2178,7 @@ private static Image CastToImageSharp(AnyBitmap bitmap)
/// Since we store ImageSharp object internal AnyBitmap (lazy) so this casting will return the same ImageSharp object if it loaded.
/// But if it is gif/tiff we need to make resize all frame to have the same size before we load to ImageSharp object;
///
- private static Image CastToImageSharp(AnyBitmap bitmap) where T :unmanaged, SixLabors.ImageSharp.PixelFormats.IPixel
+ private static Image CastToImageSharp(AnyBitmap bitmap) where T :unmanaged, IPixel
{
try
{
@@ -2225,6 +2238,7 @@ private static Image CastToImageSharp(AnyBitmap bitmap) where T :unmanaged
"Error while casting AnyBitmap to SixLabors.ImageSharp.Image", e);
}
}
+#if !NETFRAMEWORK
///
/// Implicitly casts to SixLabors.ImageSharp.Image objects from
@@ -2242,6 +2256,8 @@ public static implicit operator Image(AnyBitmap bitmap)
{
return CastToImageSharp(bitmap);
}
+#endif
+#if !NETFRAMEWORK
///
/// Implicitly casts SixLabors.ImageSharp.Image objects to
@@ -2272,6 +2288,8 @@ public static implicit operator AnyBitmap(Image image)
"Error while casting AnyBitmap from SixLabors.ImageSharp.Image", e);
}
}
+#endif
+#if !NETFRAMEWORK
///
/// Implicitly casts to SixLabors.ImageSharp.Image objects from
@@ -2302,6 +2320,8 @@ public static implicit operator Image(AnyBitmap bitmap)
"Error while casting AnyBitmap to SixLabors.ImageSharp.Image", e);
}
}
+#endif
+#if !NETFRAMEWORK
///
/// Implicitly casts SixLabors.ImageSharp.Image objects to
@@ -2332,6 +2352,8 @@ public static implicit operator AnyBitmap(Image image)
"Error while casting AnyBitmap from SixLabors.ImageSharp.Image", e);
}
}
+#endif
+#if !NETFRAMEWORK
///
/// Implicitly casts to SixLabors.ImageSharp.Image objects from
@@ -2362,6 +2384,7 @@ public static implicit operator Image(AnyBitmap bitmap)
"Error while casting AnyBitmap to SixLabors.ImageSharp.Image", e);
}
}
+#endif
///
/// Implicitly casts SkiaSharp.SKImage objects to
@@ -2908,6 +2931,13 @@ private static AnyBitmap LoadSVGImage(string file, bool preserveOriginalFormat)
DecodeSVG(file).Encode(SKEncodedImageFormat.Png, 100)
.ToArray(), preserveOriginalFormat);
}
+#if NETFRAMEWORK
+ catch (DllNotFoundException e) when (e.InnerException is FileNotFoundException)
+ {
+ // Svg.Skia is not installed; DecodeSVG's message already says so.
+ throw;
+ }
+#endif
catch (DllNotFoundException e)
{
throw new DllNotFoundException(
@@ -2924,33 +2954,49 @@ private static SKBitmap DecodeSVG(string strInput)
{
try
{
- SkiaSharp.Extended.Svg.SKSvg svg = new();
- _ = svg.Load(strInput);
-
- SKBitmap toBitmap = new(
- (int)svg.Picture.CullRect.Width,
- (int)svg.Picture.CullRect.Height);
- using (SKCanvas canvas = new(toBitmap))
- {
- canvas.Clear(SKColors.White);
- canvas.DrawPicture(svg.Picture);
- canvas.Flush();
- }
-
- return toBitmap;
-
+ return RenderSVG(strInput);
}
catch (DllNotFoundException e)
{
throw new DllNotFoundException("Please install SkiaSharp.Svg " +
"from NuGet.", e);
}
+#if NETFRAMEWORK
+ catch (FileNotFoundException e) when (e.FileName?.StartsWith("Svg.", StringComparison.OrdinalIgnoreCase) == true)
+ {
+ throw new DllNotFoundException("Please install Svg.Skia from NuGet to load SVG images on .NET Framework.", e);
+ }
+#endif
catch (Exception e)
{
throw new NotSupportedException("Error while reading SVG image format.", e);
}
}
+ // Kept separate so a missing SVG assembly surfaces inside DecodeSVG's try block (it fails when this method is JIT-compiled).
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private static SKBitmap RenderSVG(string strInput)
+ {
+#if NETFRAMEWORK
+ using Svg.Skia.SKSvg svg = new();
+#else
+ SkiaSharp.Extended.Svg.SKSvg svg = new();
+#endif
+ _ = svg.Load(strInput);
+
+ SKBitmap toBitmap = new(
+ (int)svg.Picture.CullRect.Width,
+ (int)svg.Picture.CullRect.Height);
+ using (SKCanvas canvas = new(toBitmap))
+ {
+ canvas.Clear(SKColors.White);
+ canvas.DrawPicture(svg.Picture);
+ canvas.Flush();
+ }
+
+ return toBitmap;
+ }
+
private static PlatformNotSupportedException SystemDotDrawingPlatformNotSupported(Exception innerException)
{
return new PlatformNotSupportedException($"Microsoft has chosen " +
@@ -3393,7 +3439,7 @@ private Lazy> OpenImageToImageSharp(bool preserveOriginalFo
PreserveOriginalFormat = preserveOriginalFormat;
img = ImageSharpLoader.Load(Binary);
if (Format.Name == "PNG")
- img.Mutate(img => img.BackgroundColor(SixLabors.ImageSharp.Color.White));
+ img.Mutate(img => img.BackgroundColor(Color.White));
}
CorrectImageSharp(img);
@@ -3429,15 +3475,15 @@ private void CorrectImageSharp(Image img)
// Check if image metadata is accurate already
switch (resolutionUnit)
{
- case SixLabors.ImageSharp.Metadata.PixelResolutionUnit.PixelsPerMeter:
+ case EnginePixelResolutionUnit.PixelsPerMeter:
// Convert metadata of the resolution unit to pixel per inch to match the conversion below of 1 meter = 37.3701 inches
- img.Metadata.ResolutionUnits = SixLabors.ImageSharp.Metadata.PixelResolutionUnit.PixelsPerInch;
+ img.Metadata.ResolutionUnits = EnginePixelResolutionUnit.PixelsPerInch;
img.Metadata.HorizontalResolution = Math.Ceiling(horizontal / 39.3701);
img.Metadata.VerticalResolution = Math.Ceiling(vertical / 39.3701);
break;
- case SixLabors.ImageSharp.Metadata.PixelResolutionUnit.PixelsPerCentimeter:
+ case EnginePixelResolutionUnit.PixelsPerCentimeter:
// Convert metadata of the resolution unit to pixel per inch to match the conversion below of 1 inch = 2.54 centimeters
- img.Metadata.ResolutionUnits = SixLabors.ImageSharp.Metadata.PixelResolutionUnit.PixelsPerInch;
+ img.Metadata.ResolutionUnits = EnginePixelResolutionUnit.PixelsPerInch;
img.Metadata.HorizontalResolution = Math.Ceiling(horizontal * 2.54);
img.Metadata.VerticalResolution = Math.Ceiling(vertical * 2.54);
break;
@@ -3929,22 +3975,22 @@ private static void InternalSaveAsMultiPageTiff(IEnumerable images, Strea
switch (image.Metadata.ResolutionUnits)
{
- case SixLabors.ImageSharp.Metadata.PixelResolutionUnit.AspectRatio:
+ case EnginePixelResolutionUnit.AspectRatio:
output.SetField(TiffTag.XRESOLUTION, image.Metadata.HorizontalResolution);
output.SetField(TiffTag.YRESOLUTION, image.Metadata.VerticalResolution);
output.SetField(TiffTag.RESOLUTIONUNIT, ResUnit.NONE);
break;
- case SixLabors.ImageSharp.Metadata.PixelResolutionUnit.PixelsPerInch:
+ case EnginePixelResolutionUnit.PixelsPerInch:
output.SetField(TiffTag.XRESOLUTION, image.Metadata.HorizontalResolution);
output.SetField(TiffTag.YRESOLUTION, image.Metadata.VerticalResolution);
output.SetField(TiffTag.RESOLUTIONUNIT, ResUnit.INCH);
break;
- case SixLabors.ImageSharp.Metadata.PixelResolutionUnit.PixelsPerCentimeter:
+ case EnginePixelResolutionUnit.PixelsPerCentimeter:
output.SetField(TiffTag.XRESOLUTION, image.Metadata.HorizontalResolution);
output.SetField(TiffTag.YRESOLUTION, image.Metadata.VerticalResolution);
output.SetField(TiffTag.RESOLUTIONUNIT, ResUnit.CENTIMETER);
break;
- case SixLabors.ImageSharp.Metadata.PixelResolutionUnit.PixelsPerMeter:
+ case EnginePixelResolutionUnit.PixelsPerMeter:
output.SetField(TiffTag.XRESOLUTION, image.Metadata.HorizontalResolution / 100);
output.SetField(TiffTag.YRESOLUTION, image.Metadata.VerticalResolution / 100);
output.SetField(TiffTag.RESOLUTIONUNIT, ResUnit.CENTIMETER);
diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common/Color.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common/Color.cs
index fc36d14..e93aaed 100644
--- a/IronSoftware.Drawing/IronSoftware.Drawing.Common/Color.cs
+++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common/Color.cs
@@ -1048,6 +1048,7 @@ public static implicit operator SkiaSharp.SKColor(Color color)
return new SkiaSharp.SKColor(color.R, color.G, color.B, color.A);
}
+#if !NETFRAMEWORK
///
/// Implicitly casts objects to .
/// When your .NET Class methods use as parameters or return types, you now automatically support as well.
@@ -1058,7 +1059,9 @@ public static implicit operator Color(SixLabors.ImageSharp.Color color)
string hex = color.ToHex();
return new Color(ConvertToHexNumberByte(hex, 6, 2), ConvertToHexNumberByte(hex, 0, 2), ConvertToHexNumberByte(hex, 2, 2), ConvertToHexNumberByte(hex, 4, 2));
}
+#endif
+#if !NETFRAMEWORK
///
/// Implicitly casts to objects from .
/// When your .NET Class methods use as parameters or return types, you now automatically support as well.
@@ -1072,7 +1075,9 @@ public static implicit operator SixLabors.ImageSharp.Color(Color color)
return SixLabors.ImageSharp.Color.FromRgba(color.R, color.G, color.B, color.A);
#endif
}
+#endif
+#if !NETFRAMEWORK
///
/// Implicitly casts objects to .
/// When your .NET Class methods use as parameters or return types, you now automatically support as well.
@@ -1083,7 +1088,9 @@ public static implicit operator Color(SixLabors.ImageSharp.PixelFormats.Rgba32 c
string hex = color.ToHex(); // Rgba
return new Color(ConvertToHexNumberByte(hex, 6, 2), ConvertToHexNumberByte(hex, 0, 2), ConvertToHexNumberByte(hex, 2, 2), ConvertToHexNumberByte(hex, 4, 2));
}
+#endif
+#if !NETFRAMEWORK
///
/// Implicitly casts to objects from .
/// When your .NET Class methods use as parameters or return types, you now automatically support as well.
@@ -1097,7 +1104,9 @@ public static implicit operator SixLabors.ImageSharp.PixelFormats.Rgba32(Color c
return SixLabors.ImageSharp.Color.FromRgba(color.R, color.G, color.B, color.A);
#endif
}
+#endif
+#if !NETFRAMEWORK
///
/// Implicitly casts objects to .
/// When your .NET Class methods use as parameters or return types, you now automatically support as well.
@@ -1107,7 +1116,9 @@ public static implicit operator Color(SixLabors.ImageSharp.PixelFormats.Bgra32 c
{
return new Color(color.A, color.R, color.G, color.B);
}
+#endif
+#if !NETFRAMEWORK
///
/// Implicitly casts to objects from .
/// When your .NET Class methods use as parameters or return types, you now automatically support as well.
@@ -1121,7 +1132,9 @@ public static implicit operator SixLabors.ImageSharp.PixelFormats.Bgra32(Color c
return SixLabors.ImageSharp.Color.FromRgba(color.R, color.G, color.B, color.A);
#endif
}
+#endif
+#if !NETFRAMEWORK
///
/// Implicitly casts objects to .
/// When your .NET Class methods use as parameters or return types, you now automatically support as well.
@@ -1131,7 +1144,9 @@ public static implicit operator Color(SixLabors.ImageSharp.PixelFormats.Rgb24 co
{
return new Color(color.R, color.G, color.B);
}
+#endif
+#if !NETFRAMEWORK
///
/// Implicitly casts to objects from .
/// When your .NET Class methods use as parameters or return types, you now automatically support as well.
@@ -1145,7 +1160,9 @@ public static implicit operator SixLabors.ImageSharp.PixelFormats.Rgb24(Color co
return SixLabors.ImageSharp.Color.FromRgba(color.R, color.G, color.B, color.A);
#endif
}
+#endif
+#if !NETFRAMEWORK
///
/// Implicitly casts objects to .
/// When your .NET Class methods use as parameters or return types, you now automatically support as well.
@@ -1155,7 +1172,9 @@ public static implicit operator Color(SixLabors.ImageSharp.PixelFormats.Bgr24 co
{
return new Color(color.R, color.G, color.B);
}
+#endif
+#if !NETFRAMEWORK
///
/// Implicitly casts to objects from .
/// When your .NET Class methods use as parameters or return types, you now automatically support as well.
@@ -1169,7 +1188,9 @@ public static implicit operator SixLabors.ImageSharp.PixelFormats.Bgr24(Color co
return SixLabors.ImageSharp.Color.FromRgb(color.R, color.G, color.B);
#endif
}
+#endif
+#if !NETFRAMEWORK
///
/// Implicitly casts objects to .
/// When your .NET Class methods use as parameters or return types, you now automatically support as well.
@@ -1183,7 +1204,9 @@ public static implicit operator Color(SixLabors.ImageSharp.PixelFormats.Rgb48 co
return (Color)SixLabors.ImageSharp.Color.FromRgb((byte)(color.R >> 8), (byte)(color.G >> 8), (byte)(color.B >> 8));
#endif
}
+#endif
+#if !NETFRAMEWORK
///
/// Implicitly casts to objects from .
/// When your .NET Class methods use as parameters or return types, you now automatically support as well.
@@ -1199,7 +1222,9 @@ public static implicit operator SixLabors.ImageSharp.PixelFormats.Rgb48(Color co
return result;
#endif
}
+#endif
+#if !NETFRAMEWORK
///
/// Implicitly casts objects to .
/// When your .NET Class methods use as parameters or return types, you now automatically support as well.
@@ -1209,7 +1234,9 @@ public static implicit operator Color(SixLabors.ImageSharp.PixelFormats.Rgba64 c
{
return new Color(color.A, color.R, color.G, color.B);
}
+#endif
+#if !NETFRAMEWORK
///
/// Implicitly casts to objects from .
/// When your .NET Class methods use as parameters or return types, you now automatically support as well.
@@ -1223,7 +1250,9 @@ public static implicit operator SixLabors.ImageSharp.PixelFormats.Rgba64(Color c
return SixLabors.ImageSharp.Color.FromRgba(color.R, color.G, color.B, color.A);
#endif
}
+#endif
+#if !NETFRAMEWORK
///
/// Implicitly casts objects to .
/// When your .NET Class methods use as parameters or return types, you now automatically support as well.
@@ -1233,7 +1262,9 @@ public static implicit operator Color(SixLabors.ImageSharp.PixelFormats.Abgr32 c
{
return new Color(color.A, color.R, color.G, color.B);
}
+#endif
+#if !NETFRAMEWORK
///
/// Implicitly casts to objects from .
/// When your .NET Class methods use as parameters or return types, you now automatically support as well.
@@ -1247,7 +1278,9 @@ public static implicit operator SixLabors.ImageSharp.PixelFormats.Abgr32(Color c
return SixLabors.ImageSharp.Color.FromRgba(color.R, color.G, color.B, color.A);
#endif
}
+#endif
+#if !NETFRAMEWORK
///
/// Implicitly casts objects to .
/// When your .NET Class methods use as parameters or return types, you now automatically support as well.
@@ -1257,7 +1290,9 @@ public static implicit operator Color(SixLabors.ImageSharp.PixelFormats.Argb32 c
{
return new Color(color.A, color.R, color.G, color.B);
}
+#endif
+#if !NETFRAMEWORK
///
/// Implicitly casts to objects from .
/// When your .NET Class methods use as parameters or return types, you now automatically support as well.
@@ -1271,6 +1306,7 @@ public static implicit operator SixLabors.ImageSharp.PixelFormats.Argb32(Color c
return SixLabors.ImageSharp.Color.FromRgba(color.R, color.G, color.B, color.A);
#endif
}
+#endif
///
/// Implicitly casts objects to .
diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common/ImageSharpLoader.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common/ImageSharpLoader.cs
index b520447..472e103 100644
--- a/IronSoftware.Drawing/IronSoftware.Drawing.Common/ImageSharpLoader.cs
+++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common/ImageSharpLoader.cs
@@ -1,3 +1,6 @@
+// .NET Framework has no SixLabors.ImageSharp dependency (see SkiaEngine/), so this ImageSharp-typed API
+// exists only on the other targets. AnyBitmap uses SkiaEngine.ImageSharpLoader there instead.
+#if !NETFRAMEWORK
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using System;
@@ -139,3 +142,4 @@ ulong ReadUInt64(long position)
}
}
}
+#endif
diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common/IronSoftware.Drawing.Common.csproj b/IronSoftware.Drawing/IronSoftware.Drawing.Common/IronSoftware.Drawing.Common.csproj
index f8f343d..7ab6f16 100644
--- a/IronSoftware.Drawing/IronSoftware.Drawing.Common/IronSoftware.Drawing.Common.csproj
+++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common/IronSoftware.Drawing.Common.csproj
@@ -12,7 +12,7 @@
CS8002
AnyCPU
true
- netstandard2.0;net60;net80
+ netstandard2.0;net462;net60;net80
true
@@ -26,30 +26,48 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common/Legacy/CropRectangle.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common/Legacy/CropRectangle.cs
index 5bc12f3..0332340 100644
--- a/IronSoftware.Drawing/IronSoftware.Drawing.Common/Legacy/CropRectangle.cs
+++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common/Legacy/CropRectangle.cs
@@ -201,6 +201,7 @@ public static implicit operator SkiaSharp.SKRectI(CropRectangle cropRectangle)
return SkiaSharp.SKRectI.Create(cropRectangle.X, cropRectangle.Y, cropRectangle.Width, cropRectangle.Height);
}
+#if !NETFRAMEWORK
///
/// Implicitly casts SixLabors.ImageSharp.Rectangle objects to .
/// When your .NET Class methods use as parameters and return types, you now automatically support SixLabors.ImageSharp.Rectangle as well.
@@ -210,7 +211,9 @@ public static implicit operator CropRectangle(SixLabors.ImageSharp.Rectangle rec
{
return new CropRectangle(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height);
}
+#endif
+#if !NETFRAMEWORK
///
/// Implicitly casts to SixLabors.ImageSharp.Rectangle objects from .
/// When your .NET Class methods use as parameters and return types, you now automatically support SixLabors.ImageSharp.Rectangle as well.
@@ -220,7 +223,9 @@ public static implicit operator SixLabors.ImageSharp.Rectangle(CropRectangle cro
{
return new SixLabors.ImageSharp.Rectangle(cropRectangle.X, cropRectangle.Y, cropRectangle.Width, cropRectangle.Height);
}
+#endif
+#if !NETFRAMEWORK
///
/// Implicitly casts SixLabors.ImageSharp.RectangleF objects to .
/// When your .NET Class methods use as parameters and return types, you now automatically support SixLabors.ImageSharp.RectangleF as well.
@@ -230,7 +235,9 @@ public static implicit operator CropRectangle(SixLabors.ImageSharp.RectangleF re
{
return new CropRectangle((int)rectangle.X, (int)rectangle.Y, (int)rectangle.Width, (int)rectangle.Height);
}
+#endif
+#if !NETFRAMEWORK
///
/// Implicitly casts to SixLabors.ImageSharp.RectangleF objects from .
/// When your .NET Class methods use as parameters and return types, you now automatically support SixLabors.ImageSharp.RectangleF as well.
@@ -240,6 +247,7 @@ public static implicit operator SixLabors.ImageSharp.RectangleF(CropRectangle cr
{
return new SixLabors.ImageSharp.RectangleF(cropRectangle.X, cropRectangle.Y, cropRectangle.Width, cropRectangle.Height);
}
+#endif
///
/// Implicitly casts Microsoft.Maui.Graphics.Rect objects to .
diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common/Point.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common/Point.cs
index a0a9caf..77755c3 100644
--- a/IronSoftware.Drawing/IronSoftware.Drawing.Common/Point.cs
+++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common/Point.cs
@@ -40,6 +40,7 @@ public void Offset(int dx, int dy)
#region Implicit Operators
+#if !NETFRAMEWORK
///
/// Implicitly casts SixLabors.ImageSharp.Point objects to Point
///
@@ -48,7 +49,9 @@ public static implicit operator Point(SixLabors.ImageSharp.Point point)
{
return new Point(point.X, point.Y);
}
+#endif
+#if !NETFRAMEWORK
///
/// Implicitly casts Point objects to SixLabors.ImageSharp.Point
///
@@ -57,6 +60,7 @@ public static implicit operator SixLabors.ImageSharp.Point(Point point)
{
return new SixLabors.ImageSharp.Point(point.X, point.Y);
}
+#endif
///
/// Implicitly casts System.Drawing.Point objects to .
diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common/PointF.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common/PointF.cs
index 710201f..b559db1 100644
--- a/IronSoftware.Drawing/IronSoftware.Drawing.Common/PointF.cs
+++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common/PointF.cs
@@ -40,6 +40,7 @@ public void Offset(float dx, float dy)
#region Implicit Operators
+#if !NETFRAMEWORK
///
/// Implicitly casts SixLabors.ImageSharp.PointF objects to PointF
///
@@ -48,7 +49,9 @@ public static implicit operator PointF(SixLabors.ImageSharp.PointF point)
{
return new PointF(point.X, point.Y);
}
+#endif
+#if !NETFRAMEWORK
///
/// Implicitly casts a PointF object to SixLabors.ImageSharp.PointF
///
@@ -57,6 +60,7 @@ public static implicit operator SixLabors.ImageSharp.PointF(PointF point)
{
return new SixLabors.ImageSharp.PointF(point.X, point.Y);
}
+#endif
///
/// Implicitly casts Microsoft.Maui.Graphics.Point objects to PointF
diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common/Rectangle.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common/Rectangle.cs
index fe8c345..a7f8fd3 100644
--- a/IronSoftware.Drawing/IronSoftware.Drawing.Common/Rectangle.cs
+++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common/Rectangle.cs
@@ -231,6 +231,7 @@ public static implicit operator SkiaSharp.SKRectI(Rectangle Rectangle)
return SkiaSharp.SKRectI.Create(Rectangle.X, Rectangle.Y, Rectangle.Width, Rectangle.Height);
}
+#if !NETFRAMEWORK
///
/// Implicitly casts SixLabors.ImageSharp.Rectangle objects to .
/// When your .NET Class methods use as parameters and return types, you now automatically support SixLabors.ImageSharp.Rectangle as well.
@@ -240,7 +241,9 @@ public static implicit operator Rectangle(SixLabors.ImageSharp.Rectangle rectang
{
return new Rectangle(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height);
}
+#endif
+#if !NETFRAMEWORK
///
/// Implicitly casts to SixLabors.ImageSharp.Rectangle objects from .
/// When your .NET Class methods use as parameters and return types, you now automatically support SixLabors.ImageSharp.Rectangle as well.
@@ -250,7 +253,9 @@ public static implicit operator SixLabors.ImageSharp.Rectangle(Rectangle Rectang
{
return new SixLabors.ImageSharp.Rectangle(Rectangle.X, Rectangle.Y, Rectangle.Width, Rectangle.Height);
}
+#endif
+#if !NETFRAMEWORK
///
/// Implicitly casts SixLabors.ImageSharp.RectangleF objects to .
/// When your .NET Class methods use as parameters and return types, you now automatically support SixLabors.ImageSharp.RectangleF as well.
@@ -260,7 +265,9 @@ public static implicit operator Rectangle(SixLabors.ImageSharp.RectangleF rectan
{
return new Rectangle((int)rectangle.X, (int)rectangle.Y, (int)rectangle.Width, (int)rectangle.Height);
}
+#endif
+#if !NETFRAMEWORK
///
/// Implicitly casts to SixLabors.ImageSharp.RectangleF objects from .
/// When your .NET Class methods use as parameters and return types, you now automatically support SixLabors.ImageSharp.RectangleF as well.
@@ -270,6 +277,7 @@ public static implicit operator SixLabors.ImageSharp.RectangleF(Rectangle Rectan
{
return new SixLabors.ImageSharp.RectangleF(Rectangle.X, Rectangle.Y, Rectangle.Width, Rectangle.Height);
}
+#endif
///
/// Implicitly casts Microsoft.Maui.Graphics.Rect objects to .
diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common/RectangleF.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common/RectangleF.cs
index ead79fc..75faad2 100644
--- a/IronSoftware.Drawing/IronSoftware.Drawing.Common/RectangleF.cs
+++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common/RectangleF.cs
@@ -200,6 +200,7 @@ public static implicit operator SkiaSharp.SKRect(RectangleF RectangleF)
return SkiaSharp.SKRect.Create(RectangleF.X, RectangleF.Y, RectangleF.Width, RectangleF.Height);
}
+#if !NETFRAMEWORK
///
/// Implicitly casts SixLabors.ImageSharp.RectangleF objects to .
/// When your .NET Class methods use as parameters and return types, you now automatically support SixLabors.ImageSharp.RectangleF as well.
@@ -209,7 +210,9 @@ public static implicit operator RectangleF(SixLabors.ImageSharp.RectangleF Recta
{
return new RectangleF(RectangleF.X, RectangleF.Y, RectangleF.Width, RectangleF.Height);
}
+#endif
+#if !NETFRAMEWORK
///
/// Implicitly casts to SixLabors.ImageSharp.RectangleF objects from .
/// When your .NET Class methods use as parameters and return types, you now automatically support SixLabors.ImageSharp.RectangleF as well.
@@ -219,6 +222,7 @@ public static implicit operator SixLabors.ImageSharp.RectangleF(RectangleF Recta
{
return new SixLabors.ImageSharp.RectangleF(RectangleF.X, RectangleF.Y, RectangleF.Width, RectangleF.Height);
}
+#endif
///
/// Implicitly casts Microsoft.Maui.Graphics.Rect objects to .
diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common/Size.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common/Size.cs
index 488d71e..e8dde46 100644
--- a/IronSoftware.Drawing/IronSoftware.Drawing.Common/Size.cs
+++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common/Size.cs
@@ -195,6 +195,7 @@ public Size(Point point)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator !=(Size left, Size right) => !left.Equals(right);
+#if !NETFRAMEWORK
///
/// Convert a type to a type.
///
@@ -203,7 +204,9 @@ public static implicit operator Size(SixLabors.ImageSharp.Size v)
{
return new Size(v.Width, v.Height);
}
+#endif
+#if !NETFRAMEWORK
///
/// Convert to a type.
///
@@ -212,6 +215,7 @@ public static implicit operator SixLabors.ImageSharp.Size(Size v)
{
return new SixLabors.ImageSharp.Size(v.Width, v.Height);
}
+#endif
///
/// Convert a type to a type.
diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common/SizeF.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common/SizeF.cs
index 0650543..1a09b56 100644
--- a/IronSoftware.Drawing/IronSoftware.Drawing.Common/SizeF.cs
+++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common/SizeF.cs
@@ -183,6 +183,7 @@ public static implicit operator System.Drawing.SizeF(SizeF v)
return new System.Drawing.SizeF(v.Width, v.Height);
}
+#if !NETFRAMEWORK
///
/// Convert a type to a type.
///
@@ -191,7 +192,9 @@ public static implicit operator SizeF(SixLabors.ImageSharp.SizeF v)
{
return new SizeF(v.Width, v.Height);
}
+#endif
+#if !NETFRAMEWORK
///
/// Convert to a type.
///
@@ -200,6 +203,7 @@ public static implicit operator SixLabors.ImageSharp.SizeF(SizeF v)
{
return new SixLabors.ImageSharp.SizeF(v.Width, v.Height);
}
+#endif
///
/// Convert a type to a type.
diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common/SkiaEngine/Encoders.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common/SkiaEngine/Encoders.cs
new file mode 100644
index 0000000..d0ef3e3
--- /dev/null
+++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common/SkiaEngine/Encoders.cs
@@ -0,0 +1,786 @@
+#if NETFRAMEWORK
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Compression;
+using System.Runtime.InteropServices;
+using SkiaSharp;
+
+namespace IronSoftware.Drawing.SkiaEngine
+{
+ internal interface IImageEncoder
+ {
+ void Encode(Image image, Stream stream);
+ }
+
+ internal enum JpegColorType
+ {
+ YCbCrRatio420 = 0,
+ YCbCrRatio444 = 1,
+ Luminance = 2,
+ Rgb = 3,
+ }
+
+ internal enum BmpBitsPerPixel
+ {
+ Pixel24 = 24,
+ Pixel32 = 32,
+ }
+
+ // Values match the TIFF specification (and ImageSharp's TiffCompression).
+ internal enum TiffCompression
+ {
+ Invalid = 0,
+ None = 1,
+ Ccitt1D = 2,
+ CcittGroup3Fax = 3,
+ CcittGroup4Fax = 4,
+ Lzw = 5,
+ OldJpeg = 6,
+ Jpeg = 7,
+ Deflate = 8,
+ ItuTRecT82 = 9,
+ ItuTRecT43 = 10,
+ NeXT = 32766,
+ PackBits = 32773,
+ ThunderScan = 32809,
+ OldDeflate = 32946,
+ }
+
+ internal static class ResolutionConverter
+ {
+ public static double ToPixelsPerMeter(double value, PixelResolutionUnit unit) => unit switch
+ {
+ PixelResolutionUnit.PixelsPerInch => value / 0.0254,
+ PixelResolutionUnit.PixelsPerCentimeter => value * 100,
+ _ => value,
+ };
+
+ public static double ToPixelsPerInch(double value, PixelResolutionUnit unit) => unit switch
+ {
+ PixelResolutionUnit.PixelsPerCentimeter => value * 2.54,
+ PixelResolutionUnit.PixelsPerMeter => value * 0.0254,
+ _ => value,
+ };
+ }
+
+ internal sealed class JpegEncoder : IImageEncoder
+ {
+ public int? Quality { get; set; }
+ public JpegColorType? ColorType { get; set; }
+
+ public void Encode(Image image, Stream stream)
+ {
+ // ImageSharp's JpegColorType.Rgb / YCbCrRatio444 encode without chroma subsampling.
+ bool fullChroma = ColorType == JpegColorType.Rgb || ColorType == JpegColorType.YCbCrRatio444;
+ var options = new SKJpegEncoderOptions(
+ Math.Max(1, Math.Min(100, Quality ?? 75)),
+ fullChroma ? SKJpegEncoderDownsample.Downsample444 : SKJpegEncoderDownsample.Downsample420,
+ SKJpegEncoderAlphaOption.Ignore);
+
+ byte[] encoded = SkiaEncoding.Encode(image, pixmap => pixmap.Encode(options));
+ encoded = JfifDensity.Apply(encoded, image.Metadata);
+ stream.Write(encoded, 0, encoded.Length);
+ }
+ }
+
+ internal sealed class WebpEncoder : IImageEncoder
+ {
+ public int Quality { get; set; } = 75;
+
+ public void Encode(Image image, Stream stream)
+ {
+ var options = new SKWebpEncoderOptions(SKWebpEncoderCompression.Lossy, Math.Max(0, Math.Min(100, Quality)));
+
+ byte[] encoded = SkiaEncoding.Encode(image, pixmap => pixmap.Encode(options));
+ stream.Write(encoded, 0, encoded.Length);
+ }
+ }
+
+ internal sealed class TiffEncoder : IImageEncoder
+ {
+ public TiffCompression? Compression { get; set; }
+
+ // AnyBitmap writes TIFF through BitMiracle.LibTiff (InternalSaveAsMultiPageTiff); this encoder
+ // only identifies the requested format.
+ public void Encode(Image image, Stream stream) =>
+ throw new NotSupportedException("TIFF is written by AnyBitmap through BitMiracle.LibTiff.");
+ }
+
+ internal static class SkiaEncoding
+ {
+ public static byte[] Encode(Image image, Func encode)
+ {
+ Rgba32[] pixels = image.GetFrameRgba32(0);
+ GCHandle handle = GCHandle.Alloc(pixels, GCHandleType.Pinned);
+ try
+ {
+ var info = new SKImageInfo(image.Width, image.Height, SKColorType.Rgba8888, SKAlphaType.Unpremul);
+ using var pixmap = new SKPixmap(info, handle.AddrOfPinnedObject(), image.Width * 4);
+ using SKData data = encode(pixmap) ?? throw new NotSupportedException("SkiaSharp could not encode the image.");
+ return data.ToArray();
+ }
+ finally
+ {
+ handle.Free();
+ }
+ }
+ }
+
+ /// Writes the image resolution into the JPEG JFIF APP0 segment (SkiaSharp writes a fixed one).
+ internal static class JfifDensity
+ {
+ public static byte[] Apply(byte[] jpeg, ImageMetadata metadata)
+ {
+ byte units;
+ double x = metadata.HorizontalResolution, y = metadata.VerticalResolution;
+ switch (metadata.ResolutionUnits)
+ {
+ case PixelResolutionUnit.AspectRatio: units = 0; break;
+ case PixelResolutionUnit.PixelsPerCentimeter: units = 2; break;
+ case PixelResolutionUnit.PixelsPerMeter: units = 1; x = ResolutionConverter.ToPixelsPerInch(x, PixelResolutionUnit.PixelsPerMeter); y = ResolutionConverter.ToPixelsPerInch(y, PixelResolutionUnit.PixelsPerMeter); break;
+ default: units = 1; break;
+ }
+
+ ushort xd = (ushort)Math.Max(1, Math.Min(ushort.MaxValue, Math.Round(x)));
+ ushort yd = (ushort)Math.Max(1, Math.Min(ushort.MaxValue, Math.Round(y)));
+
+ if (jpeg.Length > 20 && jpeg[2] == 0xFF && jpeg[3] == 0xE0 && jpeg[6] == 'J' && jpeg[7] == 'F' && jpeg[8] == 'I' && jpeg[9] == 'F')
+ {
+ jpeg[13] = units;
+ jpeg[14] = (byte)(xd >> 8); jpeg[15] = (byte)xd;
+ jpeg[16] = (byte)(yd >> 8); jpeg[17] = (byte)yd;
+ return jpeg;
+ }
+
+ byte[] app0 = { 0xFF, 0xE0, 0x00, 0x10, (byte)'J', (byte)'F', (byte)'I', (byte)'F', 0x00, 0x01, 0x01, units, (byte)(xd >> 8), (byte)xd, (byte)(yd >> 8), (byte)yd, 0x00, 0x00 };
+ var result = new byte[jpeg.Length + app0.Length];
+ Array.Copy(jpeg, 0, result, 0, 2);
+ Array.Copy(app0, 0, result, 2, app0.Length);
+ Array.Copy(jpeg, 2, result, 2 + app0.Length, jpeg.Length - 2);
+ return result;
+ }
+ }
+
+ /// BMP writer (32bpp BGRA with an alpha bitfield header, or 24bpp).
+ internal sealed class BmpEncoder : IImageEncoder
+ {
+ public BmpBitsPerPixel? BitsPerPixel { get; set; }
+ public bool SupportTransparency { get; set; }
+
+ public void Encode(Image image, Stream stream)
+ {
+ int width = image.Width, height = image.Height;
+ bool is32 = BitsPerPixel != BmpBitsPerPixel.Pixel24;
+ bool alphaHeader = is32 && SupportTransparency;
+ int infoHeaderSize = alphaHeader ? 108 : 40;
+ int bytesPerPixel = is32 ? 4 : 3;
+ int stride = ((width * bytesPerPixel) + 3) & ~3;
+ int imageSize = stride * height;
+ int offset = 14 + infoHeaderSize;
+
+ int xPpm = (int)Math.Round(ResolutionConverter.ToPixelsPerMeter(image.Metadata.HorizontalResolution, image.Metadata.ResolutionUnits));
+ int yPpm = (int)Math.Round(ResolutionConverter.ToPixelsPerMeter(image.Metadata.VerticalResolution, image.Metadata.ResolutionUnits));
+
+ using var writer = new BinaryWriter(stream, System.Text.Encoding.ASCII, leaveOpen: true);
+ writer.Write((byte)'B'); writer.Write((byte)'M');
+ writer.Write(offset + imageSize);
+ writer.Write(0);
+ writer.Write(offset);
+
+ writer.Write(infoHeaderSize);
+ writer.Write(width);
+ writer.Write(height); // positive: bottom-up rows
+ writer.Write((short)1);
+ writer.Write((short)(bytesPerPixel * 8));
+ writer.Write(alphaHeader ? 3 : 0); // BI_BITFIELDS : BI_RGB
+ writer.Write(imageSize);
+ writer.Write(xPpm);
+ writer.Write(yPpm);
+ writer.Write(0);
+ writer.Write(0);
+ if (alphaHeader)
+ {
+ writer.Write(0x00FF0000); // red mask
+ writer.Write(0x0000FF00); // green mask
+ writer.Write(0x000000FF); // blue mask
+ writer.Write(unchecked((int)0xFF000000)); // alpha mask
+ writer.Write(0x73524742); // 'sRGB'
+ writer.Write(new byte[36]); // endpoints
+ writer.Write(new byte[12]); // gamma
+ }
+
+ Rgba32[] pixels = image.GetFrameRgba32(0);
+ var row = new byte[stride];
+ for (int y = height - 1; y >= 0; y--)
+ {
+ int source = y * width;
+ for (int x = 0; x < width; x++)
+ {
+ Rgba32 p = pixels[source + x];
+ int o = x * bytesPerPixel;
+ row[o] = p.B;
+ row[o + 1] = p.G;
+ row[o + 2] = p.R;
+ if (is32) row[o + 3] = p.A;
+ }
+
+ writer.Write(row);
+ }
+ }
+ }
+
+ /// PNG writer that keeps the image's pixel layout (gray/RGB/RGBA, 8 or 16 bits), like ImageSharp.
+ internal sealed class PngEncoder : IImageEncoder
+ {
+ private static readonly uint[] CrcTable = BuildCrcTable();
+
+ public void Encode(Image image, Stream stream)
+ {
+ int width = image.Width, height = image.Height;
+ (byte colorType, int channels, bool sixteen) = image switch
+ {
+ Image => ((byte)0, 1, false),
+ Image => ((byte)0, 1, true),
+ Image => ((byte)4, 2, false),
+ Image => ((byte)4, 2, true),
+ Image or Image => ((byte)2, 3, false),
+ Image => ((byte)2, 3, true),
+ Image => ((byte)6, 4, true),
+ _ => ((byte)6, 4, false),
+ };
+
+ int bitDepth = sixteen ? 16 : 8;
+ int bytesPerPixel = channels * (sixteen ? 2 : 1);
+ int stride = width * bytesPerPixel;
+ byte[] palette = null, transparency = null;
+ byte[] raw;
+
+ // Like ImageSharp, keep a decoded PNG's palette or low-bit grayscale encoding.
+ int sourceType = image.Metadata.PngColorType, sourceDepth = image.Metadata.PngBitDepth;
+ if (sourceType == 3 && !sixteen && sourceDepth is 1 or 2 or 4 or 8)
+ {
+ colorType = 3;
+ bitDepth = sourceDepth;
+ raw = BuildIndexedRows(image.GetFrameRgba32(0), width, height, bitDepth, out palette, out transparency);
+ stride = (width * bitDepth + 7) / 8;
+ bytesPerPixel = 1;
+ }
+ else if (sourceType == 0 && colorType == 0 && !sixteen && sourceDepth is 1 or 2 or 4)
+ {
+ bitDepth = sourceDepth;
+ raw = BuildLowBitGrayRows(image.GetFrameRgba32(0), width, height, bitDepth);
+ stride = (width * bitDepth + 7) / 8;
+ bytesPerPixel = 1;
+ }
+ else
+ {
+ raw = BuildRows(image, channels, sixteen, stride);
+ }
+
+ stream.Write(new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }, 0, 8);
+
+ var ihdr = new byte[13];
+ WriteInt32BE(ihdr, 0, width);
+ WriteInt32BE(ihdr, 4, height);
+ ihdr[8] = (byte)bitDepth;
+ ihdr[9] = colorType;
+ WriteChunk(stream, "IHDR", ihdr);
+ if (palette != null)
+ {
+ WriteChunk(stream, "PLTE", palette);
+ if (transparency != null)
+ {
+ WriteChunk(stream, "tRNS", transparency);
+ }
+ }
+
+ var phys = new byte[9];
+ bool aspect = image.Metadata.ResolutionUnits == PixelResolutionUnit.AspectRatio;
+ WriteInt32BE(phys, 0, (int)Math.Round(aspect ? image.Metadata.HorizontalResolution : ResolutionConverter.ToPixelsPerMeter(image.Metadata.HorizontalResolution, image.Metadata.ResolutionUnits)));
+ WriteInt32BE(phys, 4, (int)Math.Round(aspect ? image.Metadata.VerticalResolution : ResolutionConverter.ToPixelsPerMeter(image.Metadata.VerticalResolution, image.Metadata.ResolutionUnits)));
+ phys[8] = (byte)(aspect ? 0 : 1);
+ WriteChunk(stream, "pHYs", phys);
+
+ WriteChunk(stream, "IDAT", Zlib(Filter(raw, height, stride, bytesPerPixel)));
+ WriteChunk(stream, "IEND", Array.Empty());
+ }
+
+ private static byte[] BuildIndexedRows(Rgba32[] pixels, int width, int height, int bitDepth, out byte[] palette, out byte[] transparency)
+ {
+ bool transparent = Array.Exists(pixels, p => p.A < 128);
+ int maxColors = (1 << bitDepth) - (transparent ? 1 : 0);
+ byte[] indices = new OctreeQuantizer(pixels, Math.Max(1, maxColors)).Map(pixels, out Rgba32[] colors, out int transparentIndex);
+
+ palette = new byte[colors.Length * 3];
+ for (int i = 0; i < colors.Length; i++)
+ {
+ palette[i * 3] = colors[i].R;
+ palette[(i * 3) + 1] = colors[i].G;
+ palette[(i * 3) + 2] = colors[i].B;
+ }
+
+ transparency = null;
+ if (transparentIndex >= 0)
+ {
+ transparency = new byte[transparentIndex + 1];
+ for (int i = 0; i < transparency.Length; i++) transparency[i] = 255;
+ transparency[transparentIndex] = 0;
+ }
+
+ return PackRows(indices, width, height, bitDepth);
+ }
+
+ private static byte[] BuildLowBitGrayRows(Rgba32[] pixels, int width, int height, int bitDepth)
+ {
+ int max = (1 << bitDepth) - 1;
+ var samples = new byte[pixels.Length];
+ for (int i = 0; i < pixels.Length; i++)
+ {
+ samples[i] = (byte)(((PixelMath.Luminance8(pixels[i].R, pixels[i].G, pixels[i].B) * max) + 127) / 255);
+ }
+
+ return PackRows(samples, width, height, bitDepth);
+ }
+
+ /// Packs one sample per pixel into rows of -bit samples, most significant bits first.
+ private static byte[] PackRows(byte[] samples, int width, int height, int bitDepth)
+ {
+ int stride = (width * bitDepth + 7) / 8;
+ if (bitDepth == 8)
+ {
+ return samples;
+ }
+
+ var packed = new byte[stride * height];
+ for (int y = 0; y < height; y++)
+ {
+ for (int x = 0; x < width; x++)
+ {
+ int bit = x * bitDepth;
+ packed[(y * stride) + (bit >> 3)] |= (byte)(samples[(y * width) + x] << (8 - bitDepth - (bit & 7)));
+ }
+ }
+
+ return packed;
+ }
+
+ private static byte[] BuildRows(Image image, int channels, bool sixteen, int stride)
+ {
+ int width = image.Width, height = image.Height;
+ var raw = new byte[stride * height];
+ if (sixteen)
+ {
+ Rgba64[] pixels = image.GetFrameRgba64(0);
+ int o = 0;
+ foreach (Rgba64 p in pixels)
+ {
+ if (channels <= 2) { WriteUInt16BE(raw, o, PixelMath.Luminance16(p.R, p.G, p.B)); o += 2; }
+ else { WriteUInt16BE(raw, o, p.R); WriteUInt16BE(raw, o + 2, p.G); WriteUInt16BE(raw, o + 4, p.B); o += 6; }
+ if (channels is 2 or 4) { WriteUInt16BE(raw, o, p.A); o += 2; }
+ }
+ }
+ else
+ {
+ Rgba32[] pixels = image.GetFrameRgba32(0);
+ int o = 0;
+ foreach (Rgba32 p in pixels)
+ {
+ if (channels <= 2) { raw[o++] = PixelMath.Luminance8(p.R, p.G, p.B); }
+ else { raw[o++] = p.R; raw[o++] = p.G; raw[o++] = p.B; }
+ if (channels is 2 or 4) { raw[o++] = p.A; }
+ }
+ }
+
+ return raw;
+ }
+
+ /// Adaptive filtering: per row, the filter with the smallest sum of absolute values.
+ private static byte[] Filter(byte[] raw, int height, int stride, int bpp)
+ {
+ var output = new byte[(stride + 1) * height];
+ var candidate = new byte[5][];
+ for (int i = 0; i < 5; i++) candidate[i] = new byte[stride];
+
+ for (int y = 0; y < height; y++)
+ {
+ int row = y * stride;
+ int prior = row - stride;
+ for (int x = 0; x < stride; x++)
+ {
+ int a = x >= bpp ? raw[row + x - bpp] : 0;
+ int b = y > 0 ? raw[prior + x] : 0;
+ int c = x >= bpp && y > 0 ? raw[prior + x - bpp] : 0;
+ int value = raw[row + x];
+ candidate[0][x] = (byte)value;
+ candidate[1][x] = (byte)(value - a);
+ candidate[2][x] = (byte)(value - b);
+ candidate[3][x] = (byte)(value - ((a + b) >> 1));
+ candidate[4][x] = (byte)(value - Paeth(a, b, c));
+ }
+
+ int best = 0;
+ long bestSum = long.MaxValue;
+ for (int f = 0; f < 5; f++)
+ {
+ long sum = 0;
+ byte[] data = candidate[f];
+ for (int x = 0; x < stride; x++) sum += data[x] < 128 ? data[x] : 256 - data[x];
+ if (sum < bestSum) { bestSum = sum; best = f; }
+ }
+
+ int o = y * (stride + 1);
+ output[o] = (byte)best;
+ Array.Copy(candidate[best], 0, output, o + 1, stride);
+ }
+
+ return output;
+ }
+
+ private static int Paeth(int a, int b, int c)
+ {
+ int p = a + b - c;
+ int pa = Math.Abs(p - a), pb = Math.Abs(p - b), pc = Math.Abs(p - c);
+ return pa <= pb && pa <= pc ? a : pb <= pc ? b : c;
+ }
+
+ private static byte[] Zlib(byte[] data)
+ {
+ using var output = new MemoryStream();
+ output.WriteByte(0x78);
+ output.WriteByte(0xDA);
+ using (var deflate = new DeflateStream(output, CompressionLevel.Optimal, leaveOpen: true))
+ {
+ deflate.Write(data, 0, data.Length);
+ }
+
+ uint a = 1, b = 0;
+ foreach (byte value in data)
+ {
+ a = (a + value) % 65521;
+ b = (b + a) % 65521;
+ }
+
+ uint adler = (b << 16) | a;
+ output.WriteByte((byte)(adler >> 24));
+ output.WriteByte((byte)(adler >> 16));
+ output.WriteByte((byte)(adler >> 8));
+ output.WriteByte((byte)adler);
+ return output.ToArray();
+ }
+
+ private static void WriteChunk(Stream stream, string type, byte[] data)
+ {
+ var header = new byte[8];
+ WriteInt32BE(header, 0, data.Length);
+ for (int i = 0; i < 4; i++) header[4 + i] = (byte)type[i];
+ stream.Write(header, 0, 8);
+ stream.Write(data, 0, data.Length);
+
+ uint crc = 0xFFFFFFFF;
+ for (int i = 4; i < 8; i++) crc = CrcTable[(crc ^ header[i]) & 0xFF] ^ (crc >> 8);
+ foreach (byte value in data) crc = CrcTable[(crc ^ value) & 0xFF] ^ (crc >> 8);
+ crc ^= 0xFFFFFFFF;
+ var crcBytes = new byte[4];
+ WriteInt32BE(crcBytes, 0, unchecked((int)crc));
+ stream.Write(crcBytes, 0, 4);
+ }
+
+ private static uint[] BuildCrcTable()
+ {
+ var table = new uint[256];
+ for (uint n = 0; n < 256; n++)
+ {
+ uint c = n;
+ for (int k = 0; k < 8; k++) c = (c & 1) != 0 ? 0xEDB88320 ^ (c >> 1) : c >> 1;
+ table[n] = c;
+ }
+
+ return table;
+ }
+
+ private static void WriteInt32BE(byte[] d, int o, int v) { d[o] = (byte)(v >> 24); d[o + 1] = (byte)(v >> 16); d[o + 2] = (byte)(v >> 8); d[o + 3] = (byte)v; }
+ private static void WriteUInt16BE(byte[] d, int o, ushort v) { d[o] = (byte)(v >> 8); d[o + 1] = (byte)v; }
+ }
+
+ /// GIF89a writer: every frame, octree-quantised to a local palette, with transparency and looping.
+ internal sealed class GifEncoder : IImageEncoder
+ {
+ public void Encode(Image image, Stream stream)
+ {
+ int width = image.Width, height = image.Height;
+ using var writer = new BinaryWriter(stream, System.Text.Encoding.ASCII, leaveOpen: true);
+ writer.Write(System.Text.Encoding.ASCII.GetBytes("GIF89a"));
+ writer.Write((ushort)width);
+ writer.Write((ushort)height);
+ writer.Write((byte)0x70); // no global colour table, 8-bit colour resolution
+ writer.Write((byte)0); // background colour index
+ writer.Write((byte)0); // pixel aspect ratio
+
+ int frameCount = image.FrameCount;
+ if (frameCount > 1)
+ {
+ writer.Write(new byte[] { 0x21, 0xFF, 0x0B });
+ writer.Write(System.Text.Encoding.ASCII.GetBytes("NETSCAPE2.0"));
+ writer.Write(new byte[] { 0x03, 0x01, 0x00, 0x00, 0x00 }); // loop forever
+ }
+
+ for (int f = 0; f < frameCount; f++)
+ {
+ Rgba32[] pixels = image.GetFrameRgba32(f);
+ var quantizer = new OctreeQuantizer(pixels, maxColors: 255);
+ byte[] indices = quantizer.Map(pixels, out Rgba32[] palette, out int transparentIndex);
+
+ int paletteBits = 1;
+ while ((1 << paletteBits) < palette.Length) paletteBits++;
+ int tableSize = 1 << paletteBits;
+
+ int delay = image.Metadata.FrameDelays != null && f < image.Metadata.FrameDelays.Count ? image.Metadata.FrameDelays[f] : 0;
+ writer.Write(new byte[] { 0x21, 0xF9, 0x04 });
+ writer.Write((byte)((transparentIndex >= 0 ? 0x01 : 0x00) | (frameCount > 1 ? 0x08 : 0x00))); // disposal: restore to background
+ writer.Write((ushort)delay);
+ writer.Write((byte)Math.Max(0, transparentIndex));
+ writer.Write((byte)0);
+
+ writer.Write((byte)0x2C);
+ writer.Write((ushort)0);
+ writer.Write((ushort)0);
+ writer.Write((ushort)width);
+ writer.Write((ushort)height);
+ writer.Write((byte)(0x80 | (paletteBits - 1))); // local colour table
+
+ for (int i = 0; i < tableSize; i++)
+ {
+ Rgba32 c = i < palette.Length ? palette[i] : default;
+ writer.Write(c.R); writer.Write(c.G); writer.Write(c.B);
+ }
+
+ int minimumCodeSize = Math.Max(2, paletteBits);
+ writer.Write((byte)minimumCodeSize);
+ LzwEncode(writer, indices, minimumCodeSize);
+ }
+
+ writer.Write((byte)0x3B);
+ }
+
+ private static void LzwEncode(BinaryWriter writer, byte[] indices, int minimumCodeSize)
+ {
+ int clearCode = 1 << minimumCodeSize;
+ int endCode = clearCode + 1;
+ int codeSize = minimumCodeSize + 1;
+ int nextCode = endCode + 1;
+ var table = new Dictionary();
+
+ var block = new byte[255];
+ int blockLength = 0;
+ int bitBuffer = 0, bitCount = 0;
+
+ void Emit(int code)
+ {
+ bitBuffer |= code << bitCount;
+ bitCount += codeSize;
+ while (bitCount >= 8)
+ {
+ block[blockLength++] = (byte)bitBuffer;
+ bitBuffer >>= 8;
+ bitCount -= 8;
+ if (blockLength == 255) { writer.Write((byte)255); writer.Write(block, 0, 255); blockLength = 0; }
+ }
+ }
+
+ Emit(clearCode);
+ if (indices.Length > 0)
+ {
+ int prefix = indices[0];
+ for (int i = 1; i < indices.Length; i++)
+ {
+ int key = (prefix << 8) | indices[i];
+ if (table.TryGetValue(key, out int code))
+ {
+ prefix = code;
+ continue;
+ }
+
+ Emit(prefix);
+
+ // Grow the code size after emitting, based on the entries added so far (as in the
+ // classic 'compress' encoder); decoders add each entry one code later.
+ if (nextCode > (1 << codeSize) - 1 && codeSize < 12) codeSize++;
+
+ if (nextCode < 4096)
+ {
+ table[key] = nextCode++;
+ }
+ else
+ {
+ Emit(clearCode);
+ table.Clear();
+ codeSize = minimumCodeSize + 1;
+ nextCode = endCode + 1;
+ }
+
+ prefix = indices[i];
+ }
+
+ Emit(prefix);
+ if (nextCode > (1 << codeSize) - 1 && codeSize < 12) codeSize++;
+ }
+
+ Emit(endCode);
+ if (bitCount > 0) { block[blockLength++] = (byte)bitBuffer; if (blockLength == 255) { writer.Write((byte)255); writer.Write(block, 0, 255); blockLength = 0; } }
+ if (blockLength > 0) { writer.Write((byte)blockLength); writer.Write(block, 0, blockLength); }
+ writer.Write((byte)0); // block terminator
+ }
+ }
+
+ /// Octree colour quantiser; pixels with alpha below 128 map to a transparent palette entry.
+ internal sealed class OctreeQuantizer
+ {
+ private sealed class Node
+ {
+ public readonly Node[] Children = new Node[8];
+ public bool IsLeaf;
+ public long R, G, B;
+ public int Count;
+ public int PaletteIndex;
+ public Node NextReducible;
+ }
+
+ private readonly Node _root = new();
+ private readonly Node[] _reducible = new Node[8];
+ private int _leafCount;
+ private readonly int _maxColors;
+
+ public OctreeQuantizer(Rgba32[] pixels, int maxColors)
+ {
+ _maxColors = maxColors;
+ foreach (Rgba32 p in pixels)
+ {
+ if (p.A < 128) continue;
+ Add(_root, p, 0);
+ while (_leafCount > _maxColors) Reduce();
+ }
+ }
+
+ public byte[] Map(Rgba32[] pixels, out Rgba32[] palette, out int transparentIndex)
+ {
+ var colors = new List();
+ BuildPalette(_root, colors);
+ bool hasTransparency = Array.Exists(pixels, p => p.A < 128);
+ transparentIndex = -1;
+ if (hasTransparency || colors.Count == 0)
+ {
+ transparentIndex = colors.Count;
+ colors.Add(new Rgba32(0, 0, 0, 0));
+ }
+
+ palette = colors.ToArray();
+ var indices = new byte[pixels.Length];
+ for (int i = 0; i < pixels.Length; i++)
+ {
+ indices[i] = pixels[i].A < 128 ? (byte)transparentIndex : (byte)Lookup(pixels[i]);
+ }
+
+ return indices;
+ }
+
+ private static int Index(Rgba32 p, int level)
+ {
+ int shift = 7 - level;
+ return (((p.R >> shift) & 1) << 2) | (((p.G >> shift) & 1) << 1) | ((p.B >> shift) & 1);
+ }
+
+ private void Add(Node node, Rgba32 p, int level)
+ {
+ if (node.IsLeaf || level == 8)
+ {
+ if (!node.IsLeaf) { node.IsLeaf = true; _leafCount++; }
+ node.R += p.R; node.G += p.G; node.B += p.B; node.Count++;
+ return;
+ }
+
+ int index = Index(p, level);
+ Node child = node.Children[index];
+ if (child == null)
+ {
+ child = new Node();
+ node.Children[index] = child;
+ if (level + 1 < 8)
+ {
+ child.NextReducible = _reducible[level + 1];
+ _reducible[level + 1] = child;
+ }
+ }
+
+ Add(child, p, level + 1);
+ }
+
+ private void Reduce()
+ {
+ int level = 7;
+ while (level > 0 && _reducible[level] == null) level--;
+ Node node = _reducible[level];
+ if (node == null) return;
+ _reducible[level] = node.NextReducible;
+
+ int removed = 0;
+ for (int i = 0; i < 8; i++)
+ {
+ Node child = node.Children[i];
+ if (child == null) continue;
+ node.R += child.R; node.G += child.G; node.B += child.B; node.Count += child.Count;
+ if (child.IsLeaf) removed++;
+ node.Children[i] = null;
+ }
+
+ node.IsLeaf = true;
+ _leafCount -= removed - 1;
+ }
+
+ private static void BuildPalette(Node node, List colors)
+ {
+ if (node.IsLeaf)
+ {
+ node.PaletteIndex = colors.Count;
+ int count = Math.Max(1, node.Count);
+ colors.Add(new Rgba32((byte)(node.R / count), (byte)(node.G / count), (byte)(node.B / count)));
+ return;
+ }
+
+ foreach (Node child in node.Children)
+ {
+ if (child != null) BuildPalette(child, colors);
+ }
+ }
+
+ private int Lookup(Rgba32 p)
+ {
+ Node node = _root;
+ for (int level = 0; level < 8 && !node.IsLeaf; level++)
+ {
+ Node next = node.Children[Index(p, level)];
+ if (next == null)
+ {
+ // Colour not in the tree (only possible for semi-transparent pixels): use any leaf below.
+ foreach (Node child in node.Children)
+ {
+ if (child != null) { next = child; break; }
+ }
+ }
+
+ node = next;
+ }
+
+ while (!node.IsLeaf)
+ {
+ foreach (Node child in node.Children)
+ {
+ if (child != null) { node = child; break; }
+ }
+ }
+
+ return node.PaletteIndex;
+ }
+ }
+}
+#endif
diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common/SkiaEngine/Formats.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common/SkiaEngine/Formats.cs
new file mode 100644
index 0000000..c4e4903
--- /dev/null
+++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common/SkiaEngine/Formats.cs
@@ -0,0 +1,224 @@
+#if NETFRAMEWORK
+using System;
+using System.Text;
+
+namespace IronSoftware.Drawing.SkiaEngine
+{
+ internal interface IImageFormat
+ {
+ string Name { get; }
+ string DefaultMimeType { get; }
+ }
+
+ // Names and MIME types match ImageSharp's formats.
+ internal sealed class PngFormat : IImageFormat { public static readonly PngFormat Instance = new(); public string Name => "PNG"; public string DefaultMimeType => "image/png"; }
+ internal sealed class JpegFormat : IImageFormat { public static readonly JpegFormat Instance = new(); public string Name => "JPEG"; public string DefaultMimeType => "image/jpeg"; }
+ internal sealed class GifFormat : IImageFormat { public static readonly GifFormat Instance = new(); public string Name => "GIF"; public string DefaultMimeType => "image/gif"; }
+ internal sealed class BmpFormat : IImageFormat { public static readonly BmpFormat Instance = new(); public string Name => "BMP"; public string DefaultMimeType => "image/bmp"; }
+ internal sealed class TiffFormat : IImageFormat { public static readonly TiffFormat Instance = new(); public string Name => "TIFF"; public string DefaultMimeType => "image/tiff"; }
+ internal sealed class WebpFormat : IImageFormat { public static readonly WebpFormat Instance = new(); public string Name => "Webp"; public string DefaultMimeType => "image/webp"; }
+
+ internal static class ImageFormats
+ {
+ public static IImageFormat Detect(byte[] data)
+ {
+ if (data == null || data.Length < 8)
+ {
+ return null;
+ }
+
+ if (data[0] == 0x89 && data[1] == 0x50 && data[2] == 0x4E && data[3] == 0x47 && data[4] == 0x0D && data[5] == 0x0A && data[6] == 0x1A && data[7] == 0x0A)
+ return PngFormat.Instance;
+ if (data[0] == 0xFF && data[1] == 0xD8 && data[2] == 0xFF)
+ return JpegFormat.Instance;
+ if (data[0] == 'G' && data[1] == 'I' && data[2] == 'F' && data[3] == '8' && (data[4] == '7' || data[4] == '9') && data[5] == 'a')
+ return GifFormat.Instance;
+ if (data[0] == 'B' && data[1] == 'M' && data.Length >= 18)
+ {
+ // Like ImageSharp, also require a known DIB header size.
+ int headerSize = BitConverter.ToInt32(data, 14);
+ if (headerSize is 12 or 16 or 40 or 52 or 56 or 64 or 108 or 124)
+ return BmpFormat.Instance;
+ }
+
+ if ((data[0] == 0x49 && data[1] == 0x49 && (data[2] == 0x2A || data[2] == 0x2B) && data[3] == 0x00)
+ || (data[0] == 0x4D && data[1] == 0x4D && data[2] == 0x00 && (data[3] == 0x2A || data[3] == 0x2B)))
+ return TiffFormat.Instance;
+ if (data.Length >= 12 && data[0] == 'R' && data[1] == 'I' && data[2] == 'F' && data[3] == 'F' && data[8] == 'W' && data[9] == 'E' && data[10] == 'B' && data[11] == 'P')
+ return WebpFormat.Instance;
+
+ return null;
+ }
+ }
+
+ /// Header information SkiaSharp does not expose: source pixel layout and resolution.
+ internal sealed class HeaderInfo
+ {
+ public int PngBitDepth;
+ public int PngColorType = -1;
+ public bool PngHasTransparency;
+ public int JpegComponents;
+ public double? HorizontalResolution;
+ public double? VerticalResolution;
+ public PixelResolutionUnit ResolutionUnits = PixelResolutionUnit.PixelsPerInch;
+
+ public static HeaderInfo Read(byte[] data, IImageFormat format)
+ {
+ var info = new HeaderInfo();
+ try
+ {
+ switch (format)
+ {
+ case PngFormat: info.ReadPng(data); break;
+ case JpegFormat: info.ReadJpeg(data); break;
+ case BmpFormat: info.ReadBmp(data); break;
+ }
+ }
+ catch (Exception)
+ {
+ // Header metadata is best effort; decoding itself is done by SkiaSharp.
+ }
+
+ return info;
+ }
+
+ private static int ReadInt32BE(byte[] d, int o) => (d[o] << 24) | (d[o + 1] << 16) | (d[o + 2] << 8) | d[o + 3];
+ private static int ReadUInt16BE(byte[] d, int o) => (d[o] << 8) | d[o + 1];
+
+ private void ReadPng(byte[] d)
+ {
+ int offset = 8;
+ while (offset + 8 <= d.Length)
+ {
+ int length = ReadInt32BE(d, offset);
+ string type = Encoding.ASCII.GetString(d, offset + 4, 4);
+ int body = offset + 8;
+ if (length < 0 || body + length > d.Length) break;
+
+ switch (type)
+ {
+ case "IHDR":
+ PngBitDepth = d[body + 8];
+ PngColorType = d[body + 9];
+ break;
+ case "tRNS":
+ PngHasTransparency = true;
+ break;
+ case "pHYs" when length >= 9:
+ int x = ReadInt32BE(d, body);
+ int y = ReadInt32BE(d, body + 4);
+ if (x > 0 && y > 0)
+ {
+ HorizontalResolution = x;
+ VerticalResolution = y;
+ ResolutionUnits = d[body + 8] == 1 ? PixelResolutionUnit.PixelsPerMeter : PixelResolutionUnit.AspectRatio;
+ }
+ break;
+ }
+
+ if (type == "IDAT" || type == "IEND") break;
+ offset = body + length + 4; // + CRC
+ }
+ }
+
+ private void ReadJpeg(byte[] d)
+ {
+ double? exifX = null, exifY = null;
+ int exifUnit = 2;
+ bool jfifSet = false;
+ int offset = 2;
+ while (offset + 4 <= d.Length)
+ {
+ if (d[offset] != 0xFF) { offset++; continue; }
+ byte marker = d[offset + 1];
+ if (marker == 0xD8 || marker == 0x01 || (marker >= 0xD0 && marker <= 0xD7)) { offset += 2; continue; }
+ if (marker == 0xD9 || marker == 0xDA) break; // end of image / start of scan
+ int length = ReadUInt16BE(d, offset + 2);
+ int body = offset + 4;
+ if (length < 2 || offset + 2 + length > d.Length) break;
+
+ if (marker == 0xE0 && length >= 16 && Encoding.ASCII.GetString(d, body, 4) == "JFIF")
+ {
+ int units = d[body + 7];
+ int xDensity = ReadUInt16BE(d, body + 8);
+ int yDensity = ReadUInt16BE(d, body + 10);
+ if (xDensity > 0 && yDensity > 0)
+ {
+ HorizontalResolution = xDensity;
+ VerticalResolution = yDensity;
+ ResolutionUnits = units switch
+ {
+ 1 => PixelResolutionUnit.PixelsPerInch,
+ 2 => PixelResolutionUnit.PixelsPerCentimeter,
+ _ => PixelResolutionUnit.AspectRatio
+ };
+ jfifSet = true;
+ }
+ }
+ else if (marker == 0xE1 && length >= 16 && Encoding.ASCII.GetString(d, body, 4) == "Exif")
+ {
+ ReadExifResolution(d, body + 6, offset + 2 + length, ref exifX, ref exifY, ref exifUnit);
+ }
+ else if (marker is 0xC0 or 0xC1 or 0xC2 or 0xC3 or 0xC5 or 0xC6 or 0xC7 or 0xC9 or 0xCA or 0xCB or 0xCD or 0xCE or 0xCF)
+ {
+ JpegComponents = d[body + 5];
+ }
+
+ offset += 2 + length;
+ }
+
+ if (!jfifSet && exifX > 0 && exifY > 0)
+ {
+ HorizontalResolution = exifX;
+ VerticalResolution = exifY;
+ ResolutionUnits = exifUnit == 3 ? PixelResolutionUnit.PixelsPerCentimeter
+ : exifUnit == 1 ? PixelResolutionUnit.AspectRatio
+ : PixelResolutionUnit.PixelsPerInch;
+ }
+ }
+
+ private static void ReadExifResolution(byte[] d, int tiffStart, int end, ref double? x, ref double? y, ref int unit)
+ {
+ bool little = d[tiffStart] == 'I';
+ int U16(int o) => little ? d[o] | (d[o + 1] << 8) : (d[o] << 8) | d[o + 1];
+ int U32(int o) => little ? d[o] | (d[o + 1] << 8) | (d[o + 2] << 16) | (d[o + 3] << 24) : (d[o] << 24) | (d[o + 1] << 16) | (d[o + 2] << 8) | d[o + 3];
+
+ int ifd = tiffStart + U32(tiffStart + 4);
+ if (ifd + 2 > end) return;
+ int count = U16(ifd);
+ for (int i = 0; i < count; i++)
+ {
+ int entry = ifd + 2 + (i * 12);
+ if (entry + 12 > end) return;
+ int tag = U16(entry);
+ if (tag == 0x011A || tag == 0x011B)
+ {
+ int valueOffset = tiffStart + U32(entry + 8);
+ if (valueOffset + 8 > end) continue;
+ double numerator = (uint)U32(valueOffset);
+ double denominator = (uint)U32(valueOffset + 4);
+ if (denominator == 0) continue;
+ if (tag == 0x011A) x = numerator / denominator; else y = numerator / denominator;
+ }
+ else if (tag == 0x0128)
+ {
+ unit = U16(entry + 8);
+ }
+ }
+ }
+
+ private void ReadBmp(byte[] d)
+ {
+ if (d.Length < 46 || BitConverter.ToInt32(d, 14) < 40) return;
+ int x = BitConverter.ToInt32(d, 38);
+ int y = BitConverter.ToInt32(d, 42);
+ if (x > 0 && y > 0)
+ {
+ HorizontalResolution = x;
+ VerticalResolution = y;
+ ResolutionUnits = PixelResolutionUnit.PixelsPerMeter;
+ }
+ }
+ }
+}
+#endif
diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common/SkiaEngine/Image.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common/SkiaEngine/Image.cs
new file mode 100644
index 0000000..08aeab0
--- /dev/null
+++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common/SkiaEngine/Image.cs
@@ -0,0 +1,553 @@
+#if NETFRAMEWORK
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+
+namespace IronSoftware.Drawing.SkiaEngine
+{
+ internal enum PixelResolutionUnit : byte
+ {
+ AspectRatio = 0,
+ PixelsPerInch = 1,
+ PixelsPerCentimeter = 2,
+ PixelsPerMeter = 3
+ }
+
+ internal sealed class ImageMetadata
+ {
+ // ImageSharp defaults.
+ public double HorizontalResolution { get; set; } = 96;
+ public double VerticalResolution { get; set; } = 96;
+ public PixelResolutionUnit ResolutionUnits { get; set; } = PixelResolutionUnit.PixelsPerInch;
+
+ /// EXIF orientation (1 = top-left). Consumed by AutoOrient.
+ internal ushort ExifOrientation { get; set; } = 1;
+
+ /// Per-frame GIF/WebP delay in 1/100 s, kept so re-encoding animations keeps timing.
+ internal List FrameDelays { get; set; }
+
+ /// Source PNG colour type (-1 when not decoded from PNG) and bit depth, kept so PNG re-encoding keeps them as ImageSharp does.
+ internal int PngColorType { get; set; } = -1;
+
+ internal int PngBitDepth { get; set; }
+
+ public ImageMetadata Clone() => new()
+ {
+ PngColorType = PngColorType,
+ PngBitDepth = PngBitDepth,
+ HorizontalResolution = HorizontalResolution,
+ VerticalResolution = VerticalResolution,
+ ResolutionUnits = ResolutionUnits,
+ ExifOrientation = ExifOrientation,
+ FrameDelays = FrameDelays == null ? null : new List(FrameDelays)
+ };
+ }
+
+ internal sealed class PixelTypeInfo
+ {
+ public PixelTypeInfo(int bitsPerPixel) => BitsPerPixel = bitsPerPixel;
+ public int BitsPerPixel { get; }
+ }
+
+ internal delegate void PixelAccessorAction(PixelAccessor accessor) where TPixel : unmanaged, IPixel;
+
+ internal readonly struct PixelAccessor where TPixel : unmanaged, IPixel
+ {
+ private readonly TPixel[] _data;
+
+ internal PixelAccessor(TPixel[] data, int width, int height)
+ {
+ _data = data;
+ Width = width;
+ Height = height;
+ }
+
+ public int Width { get; }
+ public int Height { get; }
+ public Span GetRowSpan(int rowIndex) => new(_data, rowIndex * Width, Width);
+ }
+
+ /// A frame of an image, as returned by .
+ internal sealed class ImageFrame
+ {
+ internal ImageFrame(Image owner, int index)
+ {
+ Owner = owner;
+ Index = index;
+ }
+
+ internal Image Owner { get; }
+ internal int Index { get; }
+ }
+
+ internal sealed class ImageFrameCollection
+ {
+ private readonly Image _owner;
+ internal ImageFrameCollection(Image owner) => _owner = owner;
+
+ public int Count => _owner.FrameCount;
+ public ImageFrame RootFrame => new(_owner, 0);
+ public Image CloneFrame(int index) => _owner.CloneFrameInternal(index);
+ public void AddFrame(ImageFrame frame) => _owner.AddFrameInternal(frame.Owner, frame.Index);
+ }
+
+ internal abstract class Image : IDisposable
+ {
+ public abstract int Width { get; }
+ public abstract int Height { get; }
+ public ImageMetadata Metadata { get; internal set; } = new();
+ public abstract PixelTypeInfo PixelType { get; }
+ public ImageFrameCollection Frames => new(this);
+
+ internal abstract int FrameCount { get; }
+ internal abstract Image CloneFrameInternal(int index);
+ internal abstract void AddFrameInternal(Image source, int frameIndex);
+ internal abstract Rgba32[] GetFrameRgba32(int frameIndex);
+ internal abstract Rgba64[] GetFrameRgba64(int frameIndex);
+ internal abstract Image CloneInternal();
+
+ public abstract Image CloneAs() where TOther : unmanaged, IPixel;
+
+ public Image Clone(Action operation)
+ {
+ Image clone = CloneInternal();
+ clone.Mutate(operation);
+ return clone;
+ }
+
+ public void Mutate(Action operation) => operation(new ImageProcessingContext(this));
+
+ public void Save(Stream stream, IImageEncoder encoder) => encoder.Encode(this, stream);
+
+ public void SaveAsGif(Stream stream) => Save(stream, new GifEncoder());
+
+ // Processing primitives, implemented per pixel type by Image.
+ internal abstract void ApplyCrop(Rectangle rectangle);
+ internal abstract void ApplyResize(int width, int height);
+ internal abstract void ApplyBoxPad(int width, int height, Color padColor);
+ internal abstract void ApplyRotateFlip(RotateMode rotateMode, FlipMode flipMode);
+ internal abstract void ApplyBackgroundColor(Color color);
+ internal abstract void ApplyFill(Color color, Rectangle region);
+
+ public virtual void Dispose()
+ {
+ // Pixel memory is managed; nothing to release.
+ }
+
+ public static IImageFormat DetectFormat(byte[] data) => ImageFormats.Detect(data);
+
+ public static Image Load(byte[] data) => ImageDecoder.Decode(data);
+
+ public static Image Load(string path) => ImageDecoder.Decode(File.ReadAllBytes(path));
+
+ public static Image Load(byte[] data) where TPixel : unmanaged, IPixel
+ {
+ Image image = ImageDecoder.Decode(data);
+ return image as Image ?? image.CloneAs();
+ }
+
+ public static Image LoadPixelData(ReadOnlySpan data, int width, int height) where TPixel : unmanaged, IPixel
+ {
+ var pixels = new TPixel[width * height];
+ ReadOnlySpan source = MemoryMarshal.Cast(data);
+ if (source.Length < pixels.Length)
+ {
+ throw new ArgumentException($"The pixel data is too small for a {width}x{height} image.", nameof(data));
+ }
+
+ source.Slice(0, pixels.Length).CopyTo(pixels);
+ return new Image(width, height, new List { pixels }, new ImageMetadata());
+ }
+ }
+
+ internal sealed class Image : Image where TPixel : unmanaged, IPixel
+ {
+ private static readonly PixelTypeInfo PixelInfo = new(PixelMath.BitsPerPixel());
+
+ private int _width;
+ private int _height;
+ private List _frames;
+
+ public Image(int width, int height)
+ : this(width, height, new List { new TPixel[checked(width * height)] }, new ImageMetadata())
+ {
+ }
+
+ internal Image(int width, int height, List frames, ImageMetadata metadata)
+ {
+ if (width <= 0 || height <= 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(width), $"Invalid image size {width}x{height}.");
+ }
+
+ _width = width;
+ _height = height;
+ _frames = frames;
+ Metadata = metadata ?? new ImageMetadata();
+ }
+
+ public override int Width => _width;
+ public override int Height => _height;
+ public override PixelTypeInfo PixelType => PixelInfo;
+ internal override int FrameCount => _frames.Count;
+ internal TPixel[] RootPixels => _frames[0];
+
+ public TPixel this[int x, int y]
+ {
+ get => _frames[0][(y * _width) + x];
+ set => _frames[0][(y * _width) + x] = value;
+ }
+
+ public void ProcessPixelRows(PixelAccessorAction processPixels) =>
+ processPixels(new PixelAccessor(_frames[0], _width, _height));
+
+ public void CopyPixelDataTo(Span destination) =>
+ MemoryMarshal.AsBytes(_frames[0].AsSpan()).CopyTo(destination);
+
+ public override Image CloneAs()
+ {
+ var frames = new List(_frames.Count);
+ foreach (TPixel[] frame in _frames)
+ {
+ frames.Add(Convert(frame));
+ }
+
+ return new Image(_width, _height, frames, Metadata.Clone());
+ }
+
+ internal override Image CloneInternal() =>
+ new Image(_width, _height, _frames.Select(f => (TPixel[])f.Clone()).ToList(), Metadata.Clone());
+
+ internal override Image CloneFrameInternal(int index) =>
+ new Image(_width, _height, new List { (TPixel[])_frames[index].Clone() }, Metadata.Clone());
+
+ internal override void AddFrameInternal(Image source, int frameIndex)
+ {
+ if (source.Width != _width || source.Height != _height)
+ {
+ throw new ArgumentException("Frame must have the same dimensions as the image.", nameof(source));
+ }
+
+ _frames.Add(source is Image same
+ ? (TPixel[])same._frames[frameIndex].Clone()
+ : FromRgba64(source.GetFrameRgba64(frameIndex)));
+ }
+
+ internal override Rgba32[] GetFrameRgba32(int frameIndex)
+ {
+ TPixel[] frame = _frames[frameIndex];
+ if (frame is Rgba32[] rgba)
+ {
+ return (Rgba32[])rgba.Clone();
+ }
+
+ var result = new Rgba32[frame.Length];
+ for (int i = 0; i < frame.Length; i++)
+ {
+ result[i] = frame[i].ToRgba32();
+ }
+
+ return result;
+ }
+
+ internal override Rgba64[] GetFrameRgba64(int frameIndex)
+ {
+ TPixel[] frame = _frames[frameIndex];
+ var result = new Rgba64[frame.Length];
+ for (int i = 0; i < frame.Length; i++)
+ {
+ result[i] = frame[i].ToRgba64();
+ }
+
+ return result;
+ }
+
+ private static TOther[] Convert(TPixel[] source) where TOther : unmanaged, IPixel
+ {
+ if (source is TOther[] same)
+ {
+ return (TOther[])same.Clone();
+ }
+
+ var result = new TOther[source.Length];
+ if (Is16Bit() || Is16Bit())
+ {
+ for (int i = 0; i < source.Length; i++)
+ {
+ result[i].FromRgba64(source[i].ToRgba64());
+ }
+ }
+ else
+ {
+ for (int i = 0; i < source.Length; i++)
+ {
+ result[i].FromRgba32(source[i].ToRgba32());
+ }
+ }
+
+ return result;
+ }
+
+ internal static bool Is16Bit() =>
+ typeof(T) == typeof(Rgba64) || typeof(T) == typeof(Rgb48) || typeof(T) == typeof(L16) || typeof(T) == typeof(La32);
+
+ private static TPixel[] FromRgba32(Rgba32[] source)
+ {
+ if (source is TPixel[] same)
+ {
+ return same;
+ }
+
+ var result = new TPixel[source.Length];
+ for (int i = 0; i < source.Length; i++)
+ {
+ result[i].FromRgba32(source[i]);
+ }
+
+ return result;
+ }
+
+ private static TPixel[] FromRgba64(Rgba64[] source)
+ {
+ if (source is TPixel[] same)
+ {
+ return same;
+ }
+
+ var result = new TPixel[source.Length];
+ for (int i = 0; i < source.Length; i++)
+ {
+ result[i].FromRgba64(source[i]);
+ }
+
+ return result;
+ }
+
+ internal override void ApplyCrop(Rectangle rectangle)
+ {
+ if (rectangle.Width <= 0 || rectangle.Height <= 0 || rectangle.X < 0 || rectangle.Y < 0
+ || rectangle.X + rectangle.Width > _width || rectangle.Y + rectangle.Height > _height)
+ {
+ throw new ArgumentException("Crop rectangle should be smaller than the source bounds.", nameof(rectangle));
+ }
+
+ for (int f = 0; f < _frames.Count; f++)
+ {
+ TPixel[] source = _frames[f];
+ var cropped = new TPixel[rectangle.Width * rectangle.Height];
+ for (int y = 0; y < rectangle.Height; y++)
+ {
+ Array.Copy(source, ((rectangle.Y + y) * _width) + rectangle.X, cropped, y * rectangle.Width, rectangle.Width);
+ }
+
+ _frames[f] = cropped;
+ }
+
+ _width = rectangle.Width;
+ _height = rectangle.Height;
+ }
+
+ internal override void ApplyResize(int width, int height)
+ {
+ // ImageSharp keeps the aspect ratio when one dimension is 0.
+ if (width == 0 && height > 0) width = Math.Max(1, (int)Math.Round(_width * (height / (double)_height)));
+ if (height == 0 && width > 0) height = Math.Max(1, (int)Math.Round(_height * (width / (double)_width)));
+ if (width <= 0 || height <= 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(width), "Resize dimensions must be greater than 0.");
+ }
+
+ if (width == _width && height == _height)
+ {
+ return;
+ }
+
+ bool is16 = Is16Bit();
+ for (int f = 0; f < _frames.Count; f++)
+ {
+ _frames[f] = is16
+ ? FromRgba64(SkiaOps.Resize(GetFrameRgba64(f), _width, _height, width, height))
+ : FromRgba32(SkiaOps.Resize(GetFrameRgba32(f), _width, _height, width, height));
+ }
+
+ _width = width;
+ _height = height;
+ }
+
+ internal override void ApplyBoxPad(int width, int height, Color padColor)
+ {
+ // ResizeMode.BoxPad: pad without scaling when the source fits; otherwise scale down to fit
+ // (keeping the aspect ratio) and pad. The image is centred.
+ int scaledWidth = _width;
+ int scaledHeight = _height;
+ if (_width > width || _height > height)
+ {
+ double ratio = Math.Min(width / (double)_width, height / (double)_height);
+ scaledWidth = Math.Max(1, (int)Math.Round(_width * ratio));
+ scaledHeight = Math.Max(1, (int)Math.Round(_height * ratio));
+ }
+
+ ApplyResize(scaledWidth, scaledHeight);
+
+ if (scaledWidth == width && scaledHeight == height)
+ {
+ return;
+ }
+
+ int offsetX = (width - scaledWidth) / 2;
+ int offsetY = (height - scaledHeight) / 2;
+ TPixel pad = default;
+ pad.FromRgba32(padColor);
+
+ for (int f = 0; f < _frames.Count; f++)
+ {
+ TPixel[] source = _frames[f];
+ var padded = new TPixel[width * height];
+ padded.AsSpan().Fill(pad);
+ for (int y = 0; y < scaledHeight; y++)
+ {
+ Array.Copy(source, y * scaledWidth, padded, ((offsetY + y) * width) + offsetX, scaledWidth);
+ }
+
+ _frames[f] = padded;
+ }
+
+ _width = width;
+ _height = height;
+ }
+
+ internal override void ApplyRotateFlip(RotateMode rotateMode, FlipMode flipMode)
+ {
+ int degrees = ((int)rotateMode % 360 + 360) % 360;
+ if (degrees != 0)
+ {
+ int newWidth = degrees == 180 ? _width : _height;
+ int newHeight = degrees == 180 ? _height : _width;
+ for (int f = 0; f < _frames.Count; f++)
+ {
+ TPixel[] source = _frames[f];
+ var rotated = new TPixel[source.Length];
+ for (int y = 0; y < _height; y++)
+ {
+ int row = y * _width;
+ for (int x = 0; x < _width; x++)
+ {
+ int destination = degrees switch
+ {
+ 90 => (x * newWidth) + (_height - 1 - y),
+ 180 => ((_height - 1 - y) * newWidth) + (_width - 1 - x),
+ _ => ((_width - 1 - x) * newWidth) + y, // 270
+ };
+ rotated[destination] = source[row + x];
+ }
+ }
+
+ _frames[f] = rotated;
+ }
+
+ _width = newWidth;
+ _height = newHeight;
+ }
+
+ if (flipMode == FlipMode.None)
+ {
+ return;
+ }
+
+ foreach (TPixel[] frame in _frames)
+ {
+ if (flipMode == FlipMode.Horizontal)
+ {
+ for (int y = 0; y < _height; y++)
+ {
+ Array.Reverse(frame, y * _width, _width);
+ }
+ }
+ else
+ {
+ var rowBuffer = new TPixel[_width];
+ for (int y = 0; y < _height / 2; y++)
+ {
+ int top = y * _width;
+ int bottom = (_height - 1 - y) * _width;
+ Array.Copy(frame, top, rowBuffer, 0, _width);
+ Array.Copy(frame, bottom, frame, top, _width);
+ Array.Copy(rowBuffer, 0, frame, bottom, _width);
+ }
+ }
+ }
+ }
+
+ internal override void ApplyBackgroundColor(Color color)
+ {
+ // Source-over composition of the image onto the background colour (ImageSharp BackgroundColor).
+ for (int f = 0; f < _frames.Count; f++)
+ {
+ Rgba32[] pixels = GetFrameRgba32(f);
+ for (int i = 0; i < pixels.Length; i++)
+ {
+ pixels[i] = Blend(pixels[i], color, pixels[i].A / 255f, color.A / 255f, sourceOnTop: true);
+ }
+
+ _frames[f] = FromRgba32(pixels);
+ }
+ }
+
+ internal override void ApplyFill(Color color, Rectangle region)
+ {
+ int x0 = 0, y0 = 0, x1 = _width, y1 = _height;
+ if (region != null)
+ {
+ Rectangle r = region;
+ x0 = Math.Max(0, r.X);
+ y0 = Math.Max(0, r.Y);
+ x1 = Math.Min(_width, r.X + r.Width);
+ y1 = Math.Min(_height, r.Y + r.Height);
+ if (x0 >= x1 || y0 >= y1)
+ {
+ return;
+ }
+ }
+
+ float alpha = color.A / 255f;
+ TPixel opaque = default;
+ opaque.FromRgba32(color);
+
+ foreach (TPixel[] frame in _frames)
+ {
+ for (int y = y0; y < y1; y++)
+ {
+ int row = y * _width;
+ for (int x = x0; x < x1; x++)
+ {
+ if (color.A == 255)
+ {
+ frame[row + x] = opaque;
+ }
+ else
+ {
+ Rgba32 destination = frame[row + x].ToRgba32();
+ frame[row + x].FromRgba32(Blend(color, destination, alpha, destination.A / 255f, sourceOnTop: true));
+ }
+ }
+ }
+ }
+ }
+
+ /// Porter-Duff source-over: over .
+ private static Rgba32 Blend(Rgba32 top, Rgba32 bottom, float topAlpha, float bottomAlpha, bool sourceOnTop)
+ {
+ float outAlpha = topAlpha + (bottomAlpha * (1 - topAlpha));
+ if (outAlpha <= 0)
+ {
+ return new Rgba32(0, 0, 0, 0);
+ }
+
+ byte Channel(byte t, byte b) => (byte)Math.Round(((t * topAlpha) + (b * bottomAlpha * (1 - topAlpha))) / outAlpha);
+ return new Rgba32(Channel(top.R, bottom.R), Channel(top.G, bottom.G), Channel(top.B, bottom.B), (byte)Math.Round(outAlpha * 255));
+ }
+ }
+}
+#endif
diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common/SkiaEngine/ImageDecoder.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common/SkiaEngine/ImageDecoder.cs
new file mode 100644
index 0000000..591d075
--- /dev/null
+++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common/SkiaEngine/ImageDecoder.cs
@@ -0,0 +1,297 @@
+#if NETFRAMEWORK
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+using BitMiracle.LibTiff.Classic;
+using SkiaSharp;
+
+namespace IronSoftware.Drawing.SkiaEngine
+{
+ internal static class ImageDecoder
+ {
+ private enum PixelKind { Rgba32, Rgb24, L8, L16, La16, La32, Rgb48, Rgba64 }
+
+ public static Image Decode(byte[] data)
+ {
+ IImageFormat format = ImageFormats.Detect(data)
+ ?? throw new NotSupportedException("Image cannot be loaded. The image format is not recognised.");
+
+ if (format is TiffFormat)
+ {
+ return TiffDecoder.Decode(data);
+ }
+
+ HeaderInfo header = HeaderInfo.Read(data, format);
+ using SKData skData = SKData.CreateCopy(data);
+ using SKCodec codec = SKCodec.Create(skData)
+ ?? throw new NotSupportedException($"Image cannot be loaded. The {format.Name} data could not be decoded.");
+
+ int width = codec.Info.Width;
+ int height = codec.Info.Height;
+ bool animated = format is GifFormat or WebpFormat;
+ int frameCount = animated ? Math.Max(1, codec.FrameCount) : 1;
+ SKCodecFrameInfo[] frameInfo = animated && codec.FrameCount > 0 ? codec.FrameInfo : Array.Empty();
+
+ var metadata = new ImageMetadata
+ {
+ ExifOrientation = (ushort)codec.EncodedOrigin,
+ PngColorType = header.PngColorType,
+ PngBitDepth = header.PngBitDepth
+ };
+ if (metadata.ExifOrientation is < 1 or > 8)
+ {
+ metadata.ExifOrientation = 1;
+ }
+
+ if (header.HorizontalResolution > 0 && header.VerticalResolution > 0)
+ {
+ metadata.HorizontalResolution = header.HorizontalResolution.Value;
+ metadata.VerticalResolution = header.VerticalResolution.Value;
+ metadata.ResolutionUnits = header.ResolutionUnits;
+ }
+
+ if (frameInfo.Length > 0)
+ {
+ metadata.FrameDelays = frameInfo.Select(f => Math.Max(0, f.Duration / 10)).ToList();
+ }
+
+ PixelKind kind = ChoosePixelKind(format, header);
+ bool sixteenBit = kind is PixelKind.L16 or PixelKind.La32 or PixelKind.Rgb48 or PixelKind.Rgba64;
+
+ if (sixteenBit)
+ {
+ List frames;
+ try
+ {
+ frames = DecodeFrames(codec, width, height, frameCount, frameInfo, SKColorType.Rgba16161616);
+ }
+ catch (InvalidConversionException)
+ {
+ // Some codecs cannot convert straight to 16-bit integer RGBA; go through half floats instead.
+ frames = DecodeFrames(codec, width, height, frameCount, frameInfo, SKColorType.RgbaF16).Select(HalfToRgba64).ToList();
+ }
+ return kind switch
+ {
+ PixelKind.L16 => Build(width, height, frames, metadata),
+ PixelKind.La32 => Build(width, height, frames, metadata),
+ PixelKind.Rgb48 => Build(width, height, frames, metadata),
+ _ => new Image(width, height, frames, metadata),
+ };
+ }
+
+ List frames8 = DecodeFrames(codec, width, height, frameCount, frameInfo, SKColorType.Rgba8888);
+ return kind switch
+ {
+ PixelKind.Rgb24 => Build(width, height, frames8, metadata),
+ PixelKind.L8 => Build(width, height, frames8, metadata),
+ PixelKind.La16 => Build(width, height, frames8, metadata),
+ _ => new Image(width, height, frames8, metadata),
+ };
+ }
+
+ /// The pixel type ImageSharp's non-generic Image.Load picks for the source.
+ private static PixelKind ChoosePixelKind(IImageFormat format, HeaderInfo header)
+ {
+ if (format is JpegFormat)
+ {
+ return header.JpegComponents == 1 ? PixelKind.L8 : PixelKind.Rgb24;
+ }
+
+ if (format is not PngFormat || header.PngColorType < 0)
+ {
+ return PixelKind.Rgba32;
+ }
+
+ bool sixteen = header.PngBitDepth == 16;
+ bool transparent = header.PngHasTransparency;
+ return header.PngColorType switch
+ {
+ 0 => sixteen ? (transparent ? PixelKind.La32 : PixelKind.L16) : (transparent ? PixelKind.La16 : PixelKind.L8),
+ 2 => sixteen ? (transparent ? PixelKind.Rgba64 : PixelKind.Rgb48) : (transparent ? PixelKind.Rgba32 : PixelKind.Rgb24),
+ 4 => sixteen ? PixelKind.La32 : PixelKind.La16,
+ 6 => sixteen ? PixelKind.Rgba64 : PixelKind.Rgba32,
+ _ => PixelKind.Rgba32, // palette
+ };
+ }
+
+ private static List DecodeFrames(SKCodec codec, int width, int height, int frameCount, SKCodecFrameInfo[] frameInfo, SKColorType colorType)
+ where T : unmanaged
+ {
+ var info = new SKImageInfo(width, height, colorType, SKAlphaType.Unpremul);
+ var frames = new List(frameCount);
+ for (int i = 0; i < frameCount; i++)
+ {
+ var pixels = new T[width * height];
+ int required = i < frameInfo.Length ? frameInfo[i].RequiredFrame : -1;
+ if (required >= 0 && required < frames.Count)
+ {
+ Array.Copy(frames[required], pixels, pixels.Length);
+ }
+
+ var options = new SKCodecOptions(i, required);
+ GCHandle handle = GCHandle.Alloc(pixels, GCHandleType.Pinned);
+ try
+ {
+ SKCodecResult result = codec.GetPixels(info, handle.AddrOfPinnedObject(), options);
+ if (result == SKCodecResult.InvalidConversion)
+ {
+ throw new InvalidConversionException();
+ }
+
+ if (result != SKCodecResult.Success && result != SKCodecResult.IncompleteInput)
+ {
+ throw new NotSupportedException($"Image cannot be loaded. Decoding failed: {result}.");
+ }
+ }
+ finally
+ {
+ handle.Free();
+ }
+
+ frames.Add(pixels);
+ }
+
+ return frames;
+ }
+
+ private sealed class InvalidConversionException : Exception
+ {
+ }
+
+ private static Rgba64[] HalfToRgba64(ulong[] halves)
+ {
+ var pixels = new Rgba64[halves.Length];
+ for (int i = 0; i < halves.Length; i++)
+ {
+ ulong h = halves[i];
+ pixels[i] = new Rgba64(HalfToUInt16((ushort)h), HalfToUInt16((ushort)(h >> 16)), HalfToUInt16((ushort)(h >> 32)), HalfToUInt16((ushort)(h >> 48)));
+ }
+
+ return pixels;
+ }
+
+ /// Converts an IEEE 754 half holding a 0..1 colour value to a 16-bit channel.
+ private static ushort HalfToUInt16(ushort half)
+ {
+ if ((half & 0x8000) != 0)
+ {
+ return 0;
+ }
+
+ int exponent = (half >> 10) & 0x1F;
+ int mantissa = half & 0x3FF;
+ double value = exponent == 0
+ ? mantissa / 1024.0 * Math.Pow(2, -14)
+ : exponent == 31 ? 1.0 : (1 + (mantissa / 1024.0)) * Math.Pow(2, exponent - 15);
+ return (ushort)Math.Round(Math.Min(1.0, value) * 65535);
+ }
+
+ private static Image Build(int width, int height, List frames, ImageMetadata metadata) where TPixel : unmanaged, IPixel
+ {
+ var converted = new List(frames.Count);
+ foreach (Rgba32[] frame in frames)
+ {
+ var pixels = new TPixel[frame.Length];
+ for (int i = 0; i < frame.Length; i++)
+ {
+ pixels[i].FromRgba32(frame[i]);
+ }
+
+ converted.Add(pixels);
+ }
+
+ return new Image(width, height, converted, metadata);
+ }
+
+ private static Image Build(int width, int height, List frames, ImageMetadata metadata) where TPixel : unmanaged, IPixel
+ {
+ var converted = new List(frames.Count);
+ foreach (Rgba64[] frame in frames)
+ {
+ var pixels = new TPixel[frame.Length];
+ for (int i = 0; i < frame.Length; i++)
+ {
+ pixels[i].FromRgba64(frame[i]);
+ }
+
+ converted.Add(pixels);
+ }
+
+ return new Image(width, height, converted, metadata);
+ }
+ }
+
+ /// TIFF decoding through BitMiracle.LibTiff (all pages, decoded to Rgba32).
+ internal static class TiffDecoder
+ {
+ private sealed class QuietErrorHandler : TiffErrorHandler
+ {
+ public override void WarningHandler(Tiff tif, string method, string format, params object[] args) { }
+ public override void WarningHandlerExt(Tiff tif, object clientData, string method, string format, params object[] args) { }
+ }
+
+ public static Image Decode(byte[] data)
+ {
+ Tiff.SetErrorHandler(new QuietErrorHandler());
+ using var stream = new MemoryStream(data);
+ using Tiff tiff = Tiff.ClientOpen("in-memory", "r", stream, new TiffStream())
+ ?? throw new NotSupportedException("Image cannot be loaded. The TIFF data could not be opened.");
+
+ Image result = null;
+ do
+ {
+ FieldValue[] subFileType = tiff.GetField(TiffTag.SUBFILETYPE);
+ if (subFileType != null && subFileType.Length > 0 && (FileType)subFileType[0].Value == FileType.REDUCEDIMAGE)
+ {
+ continue; // thumbnail
+ }
+
+ int width = tiff.GetField(TiffTag.IMAGEWIDTH)[0].ToInt();
+ int height = tiff.GetField(TiffTag.IMAGELENGTH)[0].ToInt();
+ var raster = new int[width * height];
+ if (!tiff.ReadRGBAImageOriented(width, height, raster, Orientation.TOPLEFT))
+ {
+ throw new NotSupportedException("Image cannot be loaded. The TIFF page could not be decoded.");
+ }
+
+ // LibTiff packs each pixel as ABGR in an int: little-endian bytes R, G, B, A = Rgba32 layout.
+ var pixels = new Rgba32[width * height];
+ MemoryMarshal.AsBytes(raster.AsSpan()).CopyTo(MemoryMarshal.AsBytes(pixels.AsSpan()));
+
+ if (result == null)
+ {
+ var metadata = new ImageMetadata();
+ FieldValue[] x = tiff.GetField(TiffTag.XRESOLUTION);
+ FieldValue[] y = tiff.GetField(TiffTag.YRESOLUTION);
+ FieldValue[] unit = tiff.GetField(TiffTag.RESOLUTIONUNIT);
+ if (x != null && y != null && x[0].ToDouble() > 0 && y[0].ToDouble() > 0)
+ {
+ metadata.HorizontalResolution = x[0].ToDouble();
+ metadata.VerticalResolution = y[0].ToDouble();
+ metadata.ResolutionUnits = unit != null && (ResUnit)unit[0].ToInt() == ResUnit.CENTIMETER
+ ? PixelResolutionUnit.PixelsPerCentimeter
+ : unit != null && (ResUnit)unit[0].ToInt() == ResUnit.NONE ? PixelResolutionUnit.AspectRatio : PixelResolutionUnit.PixelsPerInch;
+ }
+
+ result = new Image(width, height, new List { pixels }, metadata);
+ }
+ else
+ {
+ var page = new Image(width, height, new List { pixels }, new ImageMetadata());
+ if (width != result.Width || height != result.Height)
+ {
+ page.ApplyBoxPad(result.Width, result.Height, Color.Transparent);
+ }
+
+ result.AddFrameInternal(page, 0);
+ }
+ }
+ while (tiff.ReadDirectory());
+
+ return result ?? throw new NotSupportedException("Image cannot be loaded. The TIFF contains no image pages.");
+ }
+ }
+}
+#endif
diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common/SkiaEngine/ImageSharpLoader.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common/SkiaEngine/ImageSharpLoader.cs
new file mode 100644
index 0000000..3e5b85c
--- /dev/null
+++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common/SkiaEngine/ImageSharpLoader.cs
@@ -0,0 +1,16 @@
+#if NETFRAMEWORK
+namespace IronSoftware.Drawing.SkiaEngine
+{
+ ///
+ /// .NET Framework counterpart of the public IronSoftware.Drawing.ImageSharpLoader, so AnyBitmap keeps one
+ /// code path. TIFF goes through BitMiracle.LibTiff and everything else through SkiaSharp, neither of which
+ /// has ImageSharp 2.x's BigTIFF directory loop (CVE-2026-106116).
+ ///
+ internal static class ImageSharpLoader
+ {
+ public static Image Load(byte[] data) => Image.Load(data);
+
+ public static Image Load(byte[] data) where TPixel : unmanaged, IPixel => Image.Load(data);
+ }
+}
+#endif
diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common/SkiaEngine/PixelFormats.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common/SkiaEngine/PixelFormats.cs
new file mode 100644
index 0000000..b97ccaf
--- /dev/null
+++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common/SkiaEngine/PixelFormats.cs
@@ -0,0 +1,190 @@
+#if NETFRAMEWORK
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// .NET Framework image engine (SkiaSharp + LibTiff), used instead of SixLabors.ImageSharp, which is
+// end-of-life for .NET Framework. These internal types mirror the subset of the ImageSharp API that
+// AnyBitmap uses, so AnyBitmap's logic is shared between the ImageSharp and SkiaSharp builds.
+// Byte layouts match ImageSharp's pixel structs (AnyBitmap copies raw pixel bytes).
+namespace IronSoftware.Drawing.SkiaEngine
+{
+ internal interface IPixel where TSelf : unmanaged, IPixel
+ {
+ Rgba32 ToRgba32();
+ void FromRgba32(Rgba32 source);
+ Rgba64 ToRgba64();
+ void FromRgba64(Rgba64 source);
+ }
+
+ internal static class PixelMath
+ {
+ // Same rounding as ImageSharp's ColorNumerics.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static byte To8Bit(ushort component) => (byte)(((component * 255) + 32895) >> 16);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static ushort To16Bit(byte component) => (ushort)(component * 257);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static byte Luminance8(byte r, byte g, byte b) => (byte)((r * .2126F) + (g * .7152F) + (b * .0722F) + 0.5F);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static ushort Luminance16(ushort r, ushort g, ushort b) => (ushort)((r * .2126F) + (g * .7152F) + (b * .0722F) + 0.5F);
+
+ public static int BitsPerPixel() where TPixel : unmanaged, IPixel => Unsafe.SizeOf() * 8;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct Rgba32 : IPixel
+ {
+ public byte R, G, B, A;
+ public Rgba32(byte r, byte g, byte b, byte a = 255) { R = r; G = g; B = b; A = a; }
+ public Rgba32 ToRgba32() => this;
+ public void FromRgba32(Rgba32 s) => this = s;
+ public Rgba64 ToRgba64() => new(PixelMath.To16Bit(R), PixelMath.To16Bit(G), PixelMath.To16Bit(B), PixelMath.To16Bit(A));
+ public void FromRgba64(Rgba64 s) => this = s.ToRgba32();
+ public static implicit operator Color(Rgba32 p) => new(p.A, p.R, p.G, p.B);
+ public static implicit operator Rgba32(Color c) => new(c.R, c.G, c.B, c.A);
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct Bgra32 : IPixel
+ {
+ public byte B, G, R, A;
+ public Rgba32 ToRgba32() => new(R, G, B, A);
+ public void FromRgba32(Rgba32 s) { R = s.R; G = s.G; B = s.B; A = s.A; }
+ public Rgba64 ToRgba64() => ToRgba32().ToRgba64();
+ public void FromRgba64(Rgba64 s) => FromRgba32(s.ToRgba32());
+ public static implicit operator Color(Bgra32 p) => new(p.A, p.R, p.G, p.B);
+ public static implicit operator Bgra32(Color c) => new() { R = c.R, G = c.G, B = c.B, A = c.A };
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct Abgr32 : IPixel
+ {
+ public byte A, B, G, R;
+ public Rgba32 ToRgba32() => new(R, G, B, A);
+ public void FromRgba32(Rgba32 s) { R = s.R; G = s.G; B = s.B; A = s.A; }
+ public Rgba64 ToRgba64() => ToRgba32().ToRgba64();
+ public void FromRgba64(Rgba64 s) => FromRgba32(s.ToRgba32());
+ public static implicit operator Color(Abgr32 p) => new(p.A, p.R, p.G, p.B);
+ public static implicit operator Abgr32(Color c) => new() { R = c.R, G = c.G, B = c.B, A = c.A };
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct Argb32 : IPixel
+ {
+ public byte A, R, G, B;
+ public Rgba32 ToRgba32() => new(R, G, B, A);
+ public void FromRgba32(Rgba32 s) { R = s.R; G = s.G; B = s.B; A = s.A; }
+ public Rgba64 ToRgba64() => ToRgba32().ToRgba64();
+ public void FromRgba64(Rgba64 s) => FromRgba32(s.ToRgba32());
+ public static implicit operator Color(Argb32 p) => new(p.A, p.R, p.G, p.B);
+ public static implicit operator Argb32(Color c) => new() { R = c.R, G = c.G, B = c.B, A = c.A };
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct Rgb24 : IPixel
+ {
+ public byte R, G, B;
+ public Rgba32 ToRgba32() => new(R, G, B, 255);
+ public void FromRgba32(Rgba32 s) { R = s.R; G = s.G; B = s.B; }
+ public Rgba64 ToRgba64() => ToRgba32().ToRgba64();
+ public void FromRgba64(Rgba64 s) => FromRgba32(s.ToRgba32());
+ public static implicit operator Color(Rgb24 p) => new(255, p.R, p.G, p.B);
+ public static implicit operator Rgb24(Color c) => new() { R = c.R, G = c.G, B = c.B };
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct Bgr24 : IPixel
+ {
+ public byte B, G, R;
+ public Rgba32 ToRgba32() => new(R, G, B, 255);
+ public void FromRgba32(Rgba32 s) { R = s.R; G = s.G; B = s.B; }
+ public Rgba64 ToRgba64() => ToRgba32().ToRgba64();
+ public void FromRgba64(Rgba64 s) => FromRgba32(s.ToRgba32());
+ public static implicit operator Color(Bgr24 p) => new(255, p.R, p.G, p.B);
+ public static implicit operator Bgr24(Color c) => new() { R = c.R, G = c.G, B = c.B };
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct L8 : IPixel
+ {
+ public byte PackedValue;
+ public Rgba32 ToRgba32() => new(PackedValue, PackedValue, PackedValue, 255);
+ public void FromRgba32(Rgba32 s) => PackedValue = PixelMath.Luminance8(s.R, s.G, s.B);
+ public Rgba64 ToRgba64() { ushort v = PixelMath.To16Bit(PackedValue); return new(v, v, v, ushort.MaxValue); }
+ public void FromRgba64(Rgba64 s) => PackedValue = PixelMath.To8Bit(PixelMath.Luminance16(s.R, s.G, s.B));
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct L16 : IPixel
+ {
+ public ushort PackedValue;
+ public Rgba32 ToRgba32() => ToRgba64().ToRgba32();
+ public void FromRgba32(Rgba32 s) => FromRgba64(s.ToRgba64());
+ public Rgba64 ToRgba64() => new(PackedValue, PackedValue, PackedValue, ushort.MaxValue);
+ public void FromRgba64(Rgba64 s) => PackedValue = PixelMath.Luminance16(s.R, s.G, s.B);
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct La16 : IPixel
+ {
+ public byte L, A;
+ public Rgba32 ToRgba32() => new(L, L, L, A);
+ public void FromRgba32(Rgba32 s) { L = PixelMath.Luminance8(s.R, s.G, s.B); A = s.A; }
+ public Rgba64 ToRgba64() => ToRgba32().ToRgba64();
+ public void FromRgba64(Rgba64 s) => FromRgba32(s.ToRgba32());
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct La32 : IPixel
+ {
+ public ushort L, A;
+ public Rgba32 ToRgba32() => ToRgba64().ToRgba32();
+ public void FromRgba32(Rgba32 s) => FromRgba64(s.ToRgba64());
+ public Rgba64 ToRgba64() => new(L, L, L, A);
+ public void FromRgba64(Rgba64 s) { L = PixelMath.Luminance16(s.R, s.G, s.B); A = s.A; }
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct A8 : IPixel
+ {
+ public byte PackedValue;
+ public Rgba32 ToRgba32() => new(0, 0, 0, PackedValue);
+ public void FromRgba32(Rgba32 s) => PackedValue = s.A;
+ public Rgba64 ToRgba64() => new(0, 0, 0, PixelMath.To16Bit(PackedValue));
+ public void FromRgba64(Rgba64 s) => PackedValue = PixelMath.To8Bit(s.A);
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct Rgb48 : IPixel
+ {
+ public ushort R, G, B;
+ public Rgba32 ToRgba32() => ToRgba64().ToRgba32();
+ public void FromRgba32(Rgba32 s) => FromRgba64(s.ToRgba64());
+ public Rgba64 ToRgba64() => new(R, G, B, ushort.MaxValue);
+ public void FromRgba64(Rgba64 s) { R = s.R; G = s.G; B = s.B; }
+
+ // Same conversions as Color's ImageSharp operators (Color.cs).
+ public static implicit operator Color(Rgb48 p) => new(255, (byte)(p.R >> 8), (byte)(p.G >> 8), (byte)(p.B >> 8));
+ public static implicit operator Rgb48(Color c) { var r = new Rgb48(); r.FromRgba64(((Rgba32)c).ToRgba64()); return r; }
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct Rgba64 : IPixel
+ {
+ public ushort R, G, B, A;
+ public Rgba64(ushort r, ushort g, ushort b, ushort a) { R = r; G = g; B = b; A = a; }
+ public Rgba32 ToRgba32() => new(PixelMath.To8Bit(R), PixelMath.To8Bit(G), PixelMath.To8Bit(B), PixelMath.To8Bit(A));
+ public void FromRgba32(Rgba32 s) => this = s.ToRgba64();
+ public Rgba64 ToRgba64() => this;
+ public void FromRgba64(Rgba64 s) => this = s;
+
+ // Same conversions as Color's ImageSharp operators (Color.cs): the 16-bit values are passed
+ // to Color's (int, int, int, int) constructor, which truncates them to bytes.
+ public static implicit operator Color(Rgba64 p) => new(p.A, p.R, p.G, p.B);
+ public static implicit operator Rgba64(Color c) => ((Rgba32)c).ToRgba64();
+ }
+}
+#endif
diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common/SkiaEngine/Processing.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common/SkiaEngine/Processing.cs
new file mode 100644
index 0000000..3348fd8
--- /dev/null
+++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common/SkiaEngine/Processing.cs
@@ -0,0 +1,178 @@
+#if NETFRAMEWORK
+using System;
+using System.Runtime.InteropServices;
+using SkiaSharp;
+
+namespace IronSoftware.Drawing.SkiaEngine
+{
+ internal enum RotateMode
+ {
+ None = 0,
+ Rotate90 = 90,
+ Rotate180 = 180,
+ Rotate270 = 270
+ }
+
+ internal enum FlipMode
+ {
+ None = 0,
+ Horizontal = 1,
+ Vertical = 2
+ }
+
+ internal enum ResizeMode
+ {
+ Crop,
+ Pad,
+ BoxPad,
+ Max,
+ Min,
+ Stretch,
+ Manual
+ }
+
+ internal enum AnchorPositionMode
+ {
+ Center,
+ Top,
+ Bottom,
+ Left,
+ Right,
+ TopLeft,
+ TopRight,
+ BottomRight,
+ BottomLeft
+ }
+
+ internal sealed class ResizeOptions
+ {
+ public Size Size { get; set; }
+ public ResizeMode Mode { get; set; } = ResizeMode.Crop;
+ public Color PadColor { get; set; } = Color.Transparent;
+ public AnchorPositionMode Position { get; set; } = AnchorPositionMode.Center;
+ }
+
+ internal sealed class SolidBrush
+ {
+ public SolidBrush(Color color) => Color = color;
+ public Color Color { get; }
+ }
+
+ internal interface IImageProcessingContext
+ {
+ IImageProcessingContext Crop(Rectangle rectangle);
+ IImageProcessingContext Resize(int width, int height);
+ IImageProcessingContext Resize(ResizeOptions options);
+ IImageProcessingContext RotateFlip(RotateMode rotateMode, FlipMode flipMode);
+ IImageProcessingContext AutoOrient();
+ IImageProcessingContext BackgroundColor(Color color);
+ IImageProcessingContext Fill(Color color);
+ IImageProcessingContext Fill(SolidBrush brush, Rectangle rectangle);
+ }
+
+ internal sealed class ImageProcessingContext : IImageProcessingContext
+ {
+ private readonly Image _image;
+ internal ImageProcessingContext(Image image) => _image = image;
+
+ public IImageProcessingContext Crop(Rectangle rectangle) { _image.ApplyCrop(rectangle); return this; }
+
+ public IImageProcessingContext Resize(int width, int height) { _image.ApplyResize(width, height); return this; }
+
+ public IImageProcessingContext Resize(ResizeOptions options)
+ {
+ if (options.Mode == ResizeMode.BoxPad && options.Position == AnchorPositionMode.Center)
+ {
+ _image.ApplyBoxPad(options.Size.Width, options.Size.Height, options.PadColor);
+ }
+ else if (options.Mode == ResizeMode.Stretch)
+ {
+ _image.ApplyResize(options.Size.Width, options.Size.Height);
+ }
+ else
+ {
+ throw new NotSupportedException($"Resize mode {options.Mode} with anchor {options.Position} is not supported.");
+ }
+
+ return this;
+ }
+
+ public IImageProcessingContext RotateFlip(RotateMode rotateMode, FlipMode flipMode) { _image.ApplyRotateFlip(rotateMode, flipMode); return this; }
+
+ public IImageProcessingContext AutoOrient()
+ {
+ // EXIF orientation values 1-8 (same handling as ImageSharp's AutoOrient).
+ switch (_image.Metadata.ExifOrientation)
+ {
+ case 2: _image.ApplyRotateFlip(RotateMode.None, FlipMode.Horizontal); break;
+ case 3: _image.ApplyRotateFlip(RotateMode.Rotate180, FlipMode.None); break;
+ case 4: _image.ApplyRotateFlip(RotateMode.None, FlipMode.Vertical); break;
+ case 5: _image.ApplyRotateFlip(RotateMode.Rotate90, FlipMode.Horizontal); break;
+ case 6: _image.ApplyRotateFlip(RotateMode.Rotate90, FlipMode.None); break;
+ case 7: _image.ApplyRotateFlip(RotateMode.Rotate270, FlipMode.Horizontal); break;
+ case 8: _image.ApplyRotateFlip(RotateMode.Rotate270, FlipMode.None); break;
+ }
+
+ _image.Metadata.ExifOrientation = 1;
+ return this;
+ }
+
+ public IImageProcessingContext BackgroundColor(Color color) { _image.ApplyBackgroundColor(color); return this; }
+
+ public IImageProcessingContext Fill(Color color) { _image.ApplyFill(color, null); return this; }
+
+ public IImageProcessingContext Fill(SolidBrush brush, Rectangle rectangle) { _image.ApplyFill(brush.Color, rectangle); return this; }
+ }
+
+ /// SkiaSharp-backed pixel operations.
+ internal static class SkiaOps
+ {
+ // ImageSharp's default resampler is bicubic (Catmull-Rom); resampling happens in premultiplied space.
+ private static readonly SKSamplingOptions Bicubic = new(SKCubicResampler.CatmullRom);
+
+ public static Rgba32[] Resize(Rgba32[] pixels, int width, int height, int newWidth, int newHeight)
+ {
+ var result = new Rgba32[newWidth * newHeight];
+ ResizeCore(pixels, result, width, height, newWidth, newHeight, SKColorType.Rgba8888, 4);
+ return result;
+ }
+
+ public static Rgba64[] Resize(Rgba64[] pixels, int width, int height, int newWidth, int newHeight)
+ {
+ var result = new Rgba64[newWidth * newHeight];
+ ResizeCore(pixels, result, width, height, newWidth, newHeight, SKColorType.Rgba16161616, 8);
+ return result;
+ }
+
+ private static void ResizeCore(T[] source, T[] destination, int width, int height, int newWidth, int newHeight, SKColorType colorType, int bytesPerPixel)
+ where T : unmanaged
+ {
+ var sourceHandle = GCHandle.Alloc(source, GCHandleType.Pinned);
+ var destinationHandle = GCHandle.Alloc(destination, GCHandleType.Pinned);
+ try
+ {
+ var sourceInfo = new SKImageInfo(width, height, colorType, SKAlphaType.Unpremul);
+ var destinationInfo = new SKImageInfo(newWidth, newHeight, colorType, SKAlphaType.Unpremul);
+ using SKImage image = SKImage.FromPixels(sourceInfo, sourceHandle.AddrOfPinnedObject(), width * bytesPerPixel);
+ using var surface = SKSurface.Create(new SKImageInfo(newWidth, newHeight, colorType, SKAlphaType.Premul));
+ if (image == null || surface == null)
+ {
+ throw new InvalidOperationException("SkiaSharp could not allocate the resize buffers.");
+ }
+
+ using var paint = new SKPaint { BlendMode = SKBlendMode.Src };
+ surface.Canvas.DrawImage(image, new SKRect(0, 0, width, height), new SKRect(0, 0, newWidth, newHeight), Bicubic, paint);
+ if (!surface.ReadPixels(destinationInfo, destinationHandle.AddrOfPinnedObject(), newWidth * bytesPerPixel, 0, 0))
+ {
+ throw new InvalidOperationException("SkiaSharp could not read the resized pixels.");
+ }
+ }
+ finally
+ {
+ sourceHandle.Free();
+ destinationHandle.Free();
+ }
+ }
+ }
+}
+#endif
diff --git a/IronSoftware.Drawing/SystemPackages.props b/IronSoftware.Drawing/SystemPackages.props
index adb2b2c..b85b29a 100644
--- a/IronSoftware.Drawing/SystemPackages.props
+++ b/IronSoftware.Drawing/SystemPackages.props
@@ -1,13 +1,13 @@
+
+
+
+
+
+
+
+
+
+
@@ -85,6 +102,7 @@ Dependencies
+