diff --git a/CI/job_templates/build_drawing_libraries.yml b/CI/job_templates/build_drawing_libraries.yml index 6b77086..c2cd056 100644 --- a/CI/job_templates/build_drawing_libraries.yml +++ b/CI/job_templates/build_drawing_libraries.yml @@ -36,6 +36,12 @@ jobs: inputs: packageType: 'sdk' version: '8.x' + # Six Labors license (required by ImageSharp v4 / ImageSharp.Drawing v3 build-time check on the net8.0 target) + - task: DownloadSecureFile@1 + name: sixLaborsLicense + displayName: 'Download Six Labors license file' + inputs: + secureFile: 'sixlabors.lic' # Build - task: DotNetCoreCLI@2 displayName: Restore Solution NuGet Packages @@ -58,6 +64,7 @@ jobs: --no-restore --verbosity normal --property:AssemblyVersion=$(finalAssemblyVersion) + --property:SixLaborsLicenseFile=$(sixLaborsLicense.secureFilePath) versioningScheme: byEnvVar versionEnvVar: AssemblyVersion # Publish Pipeline Artifacts diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/AnyBitmapFunctionality.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/AnyBitmapFunctionality.cs index 88e2a8c..1df011b 100644 --- a/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/AnyBitmapFunctionality.cs +++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/AnyBitmapFunctionality.cs @@ -1,6 +1,7 @@ using BitMiracle.LibTiff.Classic; using FluentAssertions; using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; @@ -1155,7 +1156,11 @@ public void Resize_ShouldPreserveDepthForRepresentableFormats() Assert.Equal(24, new AnyBitmap(rgb24, 20, 20).BitsPerPixel); string path64 = "dw40_tmp64.png"; +#if NET8_0_OR_GREATER + using (var img64 = new Image(30, 30)) { img64.Save(path64, new SixLabors.ImageSharp.Formats.Png.PngEncoder { ColorType = SixLabors.ImageSharp.Formats.Png.PngColorType.RgbWithAlpha, BitDepth = SixLabors.ImageSharp.Formats.Png.PngBitDepth.Bit16 }); } +#else using (var img64 = new Image(30, 30)) { img64.SaveAsPng(path64); } +#endif try { var rgba64 = AnyBitmap.FromFile(path64); @@ -1168,6 +1173,82 @@ public void Resize_ShouldPreserveDepthForRepresentableFormats() } } + [TheoryWithAutomaticDisplayName] + [InlineData("A8", "GrayscaleWithAlpha", "Bit8", 16)] + [InlineData("L8", "Grayscale", "Bit8", 8)] + [InlineData("L16", "Grayscale", "Bit16", 16)] + [InlineData("La16", "GrayscaleWithAlpha", "Bit8", 16)] + [InlineData("La32", "GrayscaleWithAlpha", "Bit16", 32)] + [InlineData("Rgb24", "Rgb", "Bit8", 24)] + [InlineData("Bgr24", "Rgb", "Bit8", 24)] + [InlineData("Rgb48", "Rgb", "Bit16", 48)] + [InlineData("Rgba32", "RgbWithAlpha", "Bit8", 32)] + [InlineData("Bgra32", "RgbWithAlpha", "Bit8", 32)] + [InlineData("Rgba64", "RgbWithAlpha", "Bit16", 64)] + [InlineData("RgbaVector", "RgbWithAlpha", "Bit16", 64)] + [InlineData("Bgr565", "RgbWithAlpha", "Bit8", 32)] + public void ExportPng_ShouldChooseColorTypeAndBitDepthFromPixelType( + string pixelType, string expectedColorType, string expectedBitDepth, int expectedBitsPerPixel) + { + using Image source = pixelType switch + { + "A8" => new Image(4, 4), + "L8" => new Image(4, 4), + "L16" => new Image(4, 4), + "La16" => new Image(4, 4), + "La32" => new Image(4, 4), + "Rgb24" => new Image(4, 4), + "Bgr24" => new Image(4, 4), + "Rgb48" => new Image(4, 4), + "Rgba32" => new Image(4, 4), + "Bgra32" => new Image(4, 4), + "Rgba64" => new Image(4, 4), + "RgbaVector" => new Image(4, 4), + "Bgr565" => new Image(4, 4), + _ => throw new ArgumentOutOfRangeException(nameof(pixelType)) + }; + AnyBitmap bitmap = source; + + byte[] png = bitmap.ExportBytes(AnyBitmap.ImageFormat.Png); + + using (Image reloaded = Image.Load(png)) + { + var pngMetadata = reloaded.Metadata.GetPngMetadata(); + Assert.Equal(expectedColorType, pngMetadata.ColorType.ToString()); + Assert.Equal(expectedBitDepth, pngMetadata.BitDepth.ToString()); + } + Assert.Equal(expectedBitsPerPixel, AnyBitmap.FromBytes(png).BitsPerPixel); + } + + [TheoryWithAutomaticDisplayName] + [InlineData("Palette", "Bit8")] + [InlineData("Grayscale", "Bit1")] + [InlineData("Grayscale", "Bit16")] + public void ExportPng_ShouldKeepSourcePngColorTypeAndBitDepthWhenReencoding(string colorType, string bitDepth) + { + byte[] sourcePng; + using (var source = new Image(8, 8, new Rgba32(255, 255, 255, 255))) + using (var stream = new MemoryStream()) + { + source.Save(stream, new SixLabors.ImageSharp.Formats.Png.PngEncoder + { + ColorType = (SixLabors.ImageSharp.Formats.Png.PngColorType)Enum.Parse(typeof(SixLabors.ImageSharp.Formats.Png.PngColorType), colorType), + BitDepth = (SixLabors.ImageSharp.Formats.Png.PngBitDepth)Enum.Parse(typeof(SixLabors.ImageSharp.Formats.Png.PngBitDepth), bitDepth) + }); + sourcePng = stream.ToArray(); + } + + // RotateFlip decodes and re-encodes, so the output goes through the PNG encoder + // instead of returning the original bytes. + var rotated = AnyBitmap.FromBytes(sourcePng).RotateFlip(AnyBitmap.RotateMode.Rotate180, AnyBitmap.FlipMode.None); + byte[] png = rotated.ExportBytes(AnyBitmap.ImageFormat.Png); + + using Image reloaded = Image.Load(png); + var pngMetadata = reloaded.Metadata.GetPngMetadata(); + Assert.Equal(colorType, pngMetadata.ColorType.ToString()); + Assert.Equal(bitDepth, pngMetadata.BitDepth.ToString()); + } + [FactWithAutomaticDisplayName] public void Resize_ShouldPreserveFrameCountForMultiPageTiff() { @@ -1296,7 +1377,11 @@ public void Redact_ShouldRedactRegionWithColor() using var image = new Image(Configuration.Default, 100, 100, Color.White); image.Save(memoryStream, new SixLabors.ImageSharp.Formats.Bmp.BmpEncoder() { +#if NET8_0_OR_GREATER + BitsPerPixel = SixLabors.ImageSharp.Formats.Bmp.BmpBitsPerPixel.Bit32, +#else BitsPerPixel = SixLabors.ImageSharp.Formats.Bmp.BmpBitsPerPixel.Pixel32, +#endif SupportTransparency = true }); diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/ColorFunctionality.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/ColorFunctionality.cs index 41c1c7a..08bdb1d 100644 --- a/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/ColorFunctionality.cs +++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/ColorFunctionality.cs @@ -284,14 +284,22 @@ public void Cast_ImageSharp_Color_from_Color() Assert.Equal(0, red.G); Assert.Equal(0, red.B); +#if NET8_0_OR_GREATER + imgColor = SixLabors.ImageSharp.Color.FromPixel(new SixLabors.ImageSharp.PixelFormats.Rgba32(0, 255, 0, 255)); +#else imgColor = SixLabors.ImageSharp.Color.FromRgba(0, 255, 0, 255); +#endif Color green = imgColor; Assert.Equal(255, green.A); Assert.Equal(0, green.R); Assert.Equal(255, green.G); Assert.Equal(0, green.B); +#if NET8_0_OR_GREATER + imgColor = SixLabors.ImageSharp.Color.FromPixel(new SixLabors.ImageSharp.PixelFormats.Rgb24(0, 0, 255)); +#else imgColor = SixLabors.ImageSharp.Color.FromRgb(0, 0, 255); +#endif Color blue = imgColor; Assert.Equal(255, blue.A); Assert.Equal(0, blue.R); @@ -322,7 +330,11 @@ public void Cast_ImageSharp_Color_to_Color() [FactWithAutomaticDisplayName] public void Cast_ImageSharp_Rgba32_from_Color() { +#if NET8_0_OR_GREATER + SixLabors.ImageSharp.PixelFormats.Rgba32 imgColor = SixLabors.ImageSharp.Color.Red.ToPixel(); +#else SixLabors.ImageSharp.PixelFormats.Rgba32 imgColor = SixLabors.ImageSharp.Color.Red; +#endif Color red = imgColor; Assert.Equal(255, red.A); Assert.Equal(255, red.R); @@ -367,7 +379,11 @@ public void Cast_ImageSharp_Rgba32_to_Color() [FactWithAutomaticDisplayName] public void Cast_ImageSharp_Rgb24_from_Color() { +#if NET8_0_OR_GREATER + SixLabors.ImageSharp.PixelFormats.Rgb24 imgColor = SixLabors.ImageSharp.Color.Red.ToPixel(); +#else SixLabors.ImageSharp.PixelFormats.Rgb24 imgColor = SixLabors.ImageSharp.Color.Red; +#endif Color red = imgColor; Assert.Equal(255, red.R); Assert.Equal(0, red.G); @@ -470,7 +486,11 @@ public void Cast_ImageSharp_Rgb48_to_Color() [FactWithAutomaticDisplayName] public void Cast_ImageSharp_Rgba64_from_Color() { +#if NET8_0_OR_GREATER + SixLabors.ImageSharp.PixelFormats.Rgba64 imgColor = SixLabors.ImageSharp.Color.Red.ToPixel(); +#else SixLabors.ImageSharp.PixelFormats.Rgba64 imgColor = SixLabors.ImageSharp.Color.Red; +#endif Color red = imgColor; Assert.Equal(255, red.R); Assert.Equal(0, red.G); diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common/AnyBitmap.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common/AnyBitmap.cs index 655c647..f1bf65b 100644 --- a/IronSoftware.Drawing/IronSoftware.Drawing.Common/AnyBitmap.cs +++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common/AnyBitmap.cs @@ -818,7 +818,11 @@ public AnyBitmap(int width, int height, Color backgroundColor = null) var image = new Image(width, height); if (backgroundColor != null) { +#if NET8_0_OR_GREATER + image.Mutate(context => context.Paint(canvas => canvas.Fill(new SolidBrush(backgroundColor)))); +#else image.Mutate(context => context.Fill(backgroundColor)); +#endif } return [image]; }); @@ -1524,7 +1528,11 @@ public static AnyBitmap Redact( Image image = Image.Load(bitmap.Binary); Rectangle rectangle = Rectangle; var brush = new SolidBrush(color); +#if NET8_0_OR_GREATER + image.Mutate(ctx => ctx.Paint(canvas => canvas.Fill(brush, rectangle))); +#else image.Mutate(ctx => ctx.Fill(brush, rectangle)); +#endif // Redact fills a region but leaves the rest of the image untouched, so it carries the // source's declared color depth as the (decoupled, in-memory) BitsPerPixel label, @@ -3735,14 +3743,14 @@ private IImageEncoder GetDefaultImageExportEncoder(ImageFormat format = ImageFor ImageFormat.Jpeg => new JpegEncoder() { Quality = lossy, -#if NET6_0_OR_GREATER +#if NET6_0_OR_GREATER && !NET8_0_OR_GREATER ColorType = JpegEncodingColor.Rgb #else ColorType = JpegColorType.Rgb #endif }, ImageFormat.Gif => new GifEncoder(), - ImageFormat.Png => new PngEncoder(), + ImageFormat.Png => GetDefaultPngEncoder(), ImageFormat.Webp => new WebpEncoder() { Quality = lossy }, ImageFormat.Tiff => new TiffEncoder() { @@ -3753,6 +3761,40 @@ private IImageEncoder GetDefaultImageExportEncoder(ImageFormat format = ImageFor }; } + private PngEncoder GetDefaultPngEncoder() + { +#if NET8_0_OR_GREATER + // ImageSharp 3 picked the PNG color type/bit depth as: source PngMetadata if the image + // came from a PNG, otherwise a suggestion based on the pixel type. ImageSharp 4 still + // honors the source PngMetadata, but for any other source it falls back to 8-bit + // RgbWithAlpha. Reproduce the ImageSharp 3 behavior so net8 output matches net6. + Image image = GetFirstInternalImage(); + if (image.Metadata.DecodedImageFormat is PngFormat) + { + return new PngEncoder(); + } + + // Same per-pixel-type table as ImageSharp 3's encoder (anything not listed fell back + // to 8-bit RgbWithAlpha there too). + (PngColorType colorType, PngBitDepth bitDepth) = image switch + { + Image => (PngColorType.GrayscaleWithAlpha, PngBitDepth.Bit8), + Image => (PngColorType.Grayscale, PngBitDepth.Bit8), + Image => (PngColorType.Grayscale, PngBitDepth.Bit16), + Image => (PngColorType.GrayscaleWithAlpha, PngBitDepth.Bit8), + Image => (PngColorType.GrayscaleWithAlpha, PngBitDepth.Bit16), + Image or Image => (PngColorType.Rgb, PngBitDepth.Bit8), + Image => (PngColorType.Rgb, PngBitDepth.Bit16), + Image or Image => (PngColorType.RgbWithAlpha, PngBitDepth.Bit16), + _ => (PngColorType.RgbWithAlpha, PngBitDepth.Bit8) + }; + + return new PngEncoder { ColorType = colorType, BitDepth = bitDepth }; +#else + return new PngEncoder(); +#endif + } + private static ImageFormat GetImageFormat(string filename) { if (string.IsNullOrEmpty(filename)) @@ -3809,7 +3851,11 @@ object ICloneable.Clone() /// private static IImageEncoder GetDefaultImageEncoder(int imageWidth, int imageHeight) { +#if NET8_0_OR_GREATER + return new BmpEncoder { BitsPerPixel = BmpBitsPerPixel.Bit32, SupportTransparency = true }; +#else return new BmpEncoder { BitsPerPixel = BmpBitsPerPixel.Pixel32, SupportTransparency = true }; +#endif } private static void InternalSaveAsMultiPageTiff(IEnumerable images, Stream stream) diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common/Color.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common/Color.cs index 2bf4492..fc36d14 100644 --- a/IronSoftware.Drawing/IronSoftware.Drawing.Common/Color.cs +++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common/Color.cs @@ -1066,7 +1066,11 @@ public static implicit operator Color(SixLabors.ImageSharp.Color color) /// is explicitly cast to a public static implicit operator SixLabors.ImageSharp.Color(Color color) { +#if NET8_0_OR_GREATER + return SixLabors.ImageSharp.Color.FromPixel(new SixLabors.ImageSharp.PixelFormats.Rgba32(color.R, color.G, color.B, color.A)); +#else return SixLabors.ImageSharp.Color.FromRgba(color.R, color.G, color.B, color.A); +#endif } /// @@ -1087,7 +1091,11 @@ public static implicit operator Color(SixLabors.ImageSharp.PixelFormats.Rgba32 c /// is explicitly cast to a public static implicit operator SixLabors.ImageSharp.PixelFormats.Rgba32(Color color) { +#if NET8_0_OR_GREATER + return new SixLabors.ImageSharp.PixelFormats.Rgba32(color.R, color.G, color.B, color.A); +#else return SixLabors.ImageSharp.Color.FromRgba(color.R, color.G, color.B, color.A); +#endif } /// @@ -1107,7 +1115,11 @@ public static implicit operator Color(SixLabors.ImageSharp.PixelFormats.Bgra32 c /// is explicitly cast to a public static implicit operator SixLabors.ImageSharp.PixelFormats.Bgra32(Color color) { +#if NET8_0_OR_GREATER + return new SixLabors.ImageSharp.PixelFormats.Bgra32(color.R, color.G, color.B, color.A); +#else return SixLabors.ImageSharp.Color.FromRgba(color.R, color.G, color.B, color.A); +#endif } /// @@ -1127,7 +1139,11 @@ public static implicit operator Color(SixLabors.ImageSharp.PixelFormats.Rgb24 co /// is explicitly cast to a public static implicit operator SixLabors.ImageSharp.PixelFormats.Rgb24(Color color) { +#if NET8_0_OR_GREATER + return new SixLabors.ImageSharp.PixelFormats.Rgb24(color.R, color.G, color.B); +#else return SixLabors.ImageSharp.Color.FromRgba(color.R, color.G, color.B, color.A); +#endif } /// @@ -1147,7 +1163,11 @@ public static implicit operator Color(SixLabors.ImageSharp.PixelFormats.Bgr24 co /// is explicitly cast to a public static implicit operator SixLabors.ImageSharp.PixelFormats.Bgr24(Color color) { +#if NET8_0_OR_GREATER + return new SixLabors.ImageSharp.PixelFormats.Bgr24(color.R, color.G, color.B); +#else return SixLabors.ImageSharp.Color.FromRgb(color.R, color.G, color.B); +#endif } /// @@ -1157,7 +1177,11 @@ public static implicit operator SixLabors.ImageSharp.PixelFormats.Bgr24(Color co /// will automatically be casted to public static implicit operator Color(SixLabors.ImageSharp.PixelFormats.Rgb48 color) { +#if NET8_0_OR_GREATER + return (Color)SixLabors.ImageSharp.Color.FromPixel(new SixLabors.ImageSharp.PixelFormats.Rgb24((byte)(color.R >> 8), (byte)(color.G >> 8), (byte)(color.B >> 8))); +#else return (Color)SixLabors.ImageSharp.Color.FromRgb((byte)(color.R >> 8), (byte)(color.G >> 8), (byte)(color.B >> 8)); +#endif } /// @@ -1167,9 +1191,13 @@ public static implicit operator Color(SixLabors.ImageSharp.PixelFormats.Rgb48 co /// is explicitly cast to a public static implicit operator SixLabors.ImageSharp.PixelFormats.Rgb48(Color color) { +#if NET8_0_OR_GREATER + return SixLabors.ImageSharp.PixelFormats.Rgb48.FromRgba64((SixLabors.ImageSharp.PixelFormats.Rgba64)color); +#else var result = new SixLabors.ImageSharp.PixelFormats.Rgb48(); result.FromRgba64((SixLabors.ImageSharp.PixelFormats.Rgba64)color); return result; +#endif } /// @@ -1189,7 +1217,11 @@ public static implicit operator Color(SixLabors.ImageSharp.PixelFormats.Rgba64 c /// is explicitly cast to a public static implicit operator SixLabors.ImageSharp.PixelFormats.Rgba64(Color color) { +#if NET8_0_OR_GREATER + return new SixLabors.ImageSharp.PixelFormats.Rgba64(new SixLabors.ImageSharp.PixelFormats.Rgba32(color.R, color.G, color.B, color.A)); +#else return SixLabors.ImageSharp.Color.FromRgba(color.R, color.G, color.B, color.A); +#endif } /// @@ -1209,7 +1241,11 @@ public static implicit operator Color(SixLabors.ImageSharp.PixelFormats.Abgr32 c /// is explicitly cast to a public static implicit operator SixLabors.ImageSharp.PixelFormats.Abgr32(Color color) { +#if NET8_0_OR_GREATER + return new SixLabors.ImageSharp.PixelFormats.Abgr32(color.R, color.G, color.B, color.A); +#else return SixLabors.ImageSharp.Color.FromRgba(color.R, color.G, color.B, color.A); +#endif } /// @@ -1229,7 +1265,11 @@ public static implicit operator Color(SixLabors.ImageSharp.PixelFormats.Argb32 c /// is explicitly cast to a public static implicit operator SixLabors.ImageSharp.PixelFormats.Argb32(Color color) { +#if NET8_0_OR_GREATER + return new SixLabors.ImageSharp.PixelFormats.Argb32(color.R, color.G, color.B, color.A); +#else return SixLabors.ImageSharp.Color.FromRgba(color.R, color.G, color.B, color.A); +#endif } /// diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common/IronSoftware.Drawing.Common.csproj b/IronSoftware.Drawing/IronSoftware.Drawing.Common/IronSoftware.Drawing.Common.csproj index c696cb3..5762256 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 + netstandard2.0;net60;net80 true @@ -32,6 +32,12 @@ + + + + + + diff --git a/NuGet/IronSoftware.Drawing.nuspec b/NuGet/IronSoftware.Drawing.nuspec index 0e64b70..a164f8c 100644 --- a/NuGet/IronSoftware.Drawing.nuspec +++ b/NuGet/IronSoftware.Drawing.nuspec @@ -42,11 +42,7 @@ For general support and technical inquiries, please email us at: support@ironsof IronSoftware.System.Drawing is an open-source solution for .NET developers to replace System.Drawing.Common with a universal and flexible library. Features -- Added support for loading TIFF and BigTIFF files larger than 2 GB using a streaming loader, enabling page-by-page decoding without loading the entire file into memory -Enhancements -- AnyBitmap now preserves the original source color depth (BitsPerPixel) across derived operations, including frame extraction, cloning, rotation, redaction, and resizing -Bug Fixes -- Fixed an issue where derived AnyBitmap operations incorrectly reported a decoded 32bpp color depth instead of the original source pixel depth +- Added a .NET 8 target using ImageSharp 4.1.2 and Drawing 3.1.2, enabling security fixes including CVE-2026-106114. Other targets remain unchanged. Copyright © Iron Software 2022-2026 Images, Bitmap, SkiaSharp, SixLabors, BitMiracle, Maui, SVG, TIFF, TIF, GIF, JPEG, PNG, Color, Rectangle, Drawing, C#, VB.NET, ASPX, create, render, generate, standard, netstandard2.0, core, netcore @@ -64,6 +60,12 @@ Bug Fixes + + + + + + @@ -73,6 +75,7 @@ Bug Fixes + \ No newline at end of file