diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/AnyBitmapFunctionality.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/AnyBitmapFunctionality.cs index 83f30a5..76a6671 100644 --- a/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/AnyBitmapFunctionality.cs +++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/AnyBitmapFunctionality.cs @@ -26,7 +26,14 @@ public void Create_AnyBitmap_by_Filename() string imagePath = GetRelativeFilePath("Mona-Lisa-oil-wood-panel-Leonardo-da.webp"); var bitmap = AnyBitmap.FromFile(imagePath); + bitmap.IsImageLoaded().Should().BeFalse(); + bitmap.SaveAs("result.bmp"); + + bitmap.IsImageLoaded().Should().BeTrue(); + //should still be the original bytes + bitmap.Length.Should().Be((int)new FileInfo(imagePath).Length); + Assert.Equal(671, bitmap.Width); Assert.Equal(1000, bitmap.Height); Assert.Equal(74684, bitmap.Length); @@ -47,7 +54,14 @@ public void Create_AnyBitmap_by_Byte() byte[] bytes = File.ReadAllBytes(imagePath); var bitmap = AnyBitmap.FromBytes(bytes); + bitmap.IsImageLoaded().Should().BeFalse(); + _ = bitmap.TrySaveAs("result.bmp"); + + bitmap.IsImageLoaded().Should().BeTrue(); + //should still be the original bytes + bitmap.Length.Should().Be(bytes.Length); + AssertImageAreEqual(imagePath, "result.bmp"); bitmap = new AnyBitmap(bytes); @@ -63,7 +77,14 @@ public void Create_AnyBitmap_by_Stream() Stream ms = new MemoryStream(bytes); var bitmap = AnyBitmap.FromStream(ms); + bitmap.IsImageLoaded().Should().BeFalse(); + _ = bitmap.TrySaveAs("result.bmp"); + + bitmap.IsImageLoaded().Should().BeTrue(); + //should still be the original bytes + bitmap.Length.Should().Be(bytes.Length); + AssertImageAreEqual(imagePath, "result.bmp"); ms.Position = 0; @@ -80,12 +101,21 @@ public void Create_AnyBitmap_by_MemoryStream() var ms = new MemoryStream(bytes); var bitmap = AnyBitmap.FromStream(ms); + bitmap.IsImageLoaded().Should().BeFalse(); + _ = bitmap.TrySaveAs("result.bmp"); + + bitmap.IsImageLoaded().Should().BeTrue(); + //should still be the original bytes + bitmap.Length.Should().Be(bytes.Length); + AssertImageAreEqual(imagePath, "result.bmp"); bitmap = new AnyBitmap(ms); bitmap.SaveAs("result.bmp"); AssertImageAreEqual(imagePath, "result.bmp"); + + } [FactWithAutomaticDisplayName] @@ -245,6 +275,16 @@ public void AnyBitmap_should_set_Pixel() // Check the pixel color has changed Assert.Equal(bitmap.GetPixel(0, 0), Color.Black); + +#if NETFRAMEWORK + //windows only + // SetPixel makes the image dirty so it should update AnyBitmap.Binary value + + System.Drawing.Bitmap temp1 = bitmap; + AnyBitmap temp2 = (AnyBitmap)temp1; + Assert.Equal(temp1.GetPixel(0, 0).ToArgb(), System.Drawing.Color.Black.ToArgb()); + Assert.Equal(temp2.GetPixel(0, 0), Color.Black); +#endif } } @@ -427,10 +467,14 @@ public void CastSixLabors_to_AnyBitmap() AssertImageAreEqual("expected.bmp", "result.bmp", true); } - [FactWithAutomaticDisplayName] - public void CastSixLabors_from_AnyBitmap() + [TheoryWithAutomaticDisplayName] + [InlineData("mountainclimbers.jpg")] + [InlineData("van-gogh-starry-night-vincent-van-gogh.jpg")] + [InlineData("animated_qr.gif")] + [InlineData("Sample-Tiff-File-download-for-Testing.tiff")] + public void CastSixLabors_from_AnyBitmap(string filename) { - var anyBitmap = AnyBitmap.FromFile(GetRelativeFilePath("mountainclimbers.jpg")); + var anyBitmap = AnyBitmap.FromFile(GetRelativeFilePath(filename)); Image imgSharp = anyBitmap; anyBitmap.SaveAs("expected.bmp"); @@ -439,6 +483,38 @@ public void CastSixLabors_from_AnyBitmap() AssertImageAreEqual("expected.bmp", "result.bmp", true); } + [TheoryWithAutomaticDisplayName] + [InlineData("mountainclimbers.jpg")] + [InlineData("van-gogh-starry-night-vincent-van-gogh.jpg")] + [InlineData("animated_qr.gif")] + [InlineData("Sample-Tiff-File-download-for-Testing.tiff")] + public void CastSixLabors_from_AnyBitmap_Rgb24(string filename) + { + var anyBitmap = AnyBitmap.FromFile(GetRelativeFilePath(filename)); + Image imgSharp = anyBitmap; + + anyBitmap.SaveAs("expected.bmp"); + imgSharp.Save("result.bmp"); + + AssertImageAreEqual("expected.bmp", "result.bmp", true); + } + + [TheoryWithAutomaticDisplayName] + [InlineData("mountainclimbers.jpg")] + [InlineData("van-gogh-starry-night-vincent-van-gogh.jpg")] + [InlineData("animated_qr.gif")] + [InlineData("Sample-Tiff-File-download-for-Testing.tiff")] + public void CastSixLabors_from_AnyBitmap_Rgba32(string filename) + { + var anyBitmap = AnyBitmap.FromFile(GetRelativeFilePath(filename)); + Image imgSharp = anyBitmap; + + anyBitmap.SaveAs("expected.bmp"); + imgSharp.Save("result.bmp"); + + AssertImageAreEqual("expected.bmp", "result.bmp", true); + } + [FactWithAutomaticDisplayName] public void CastBitmap_to_AnyBitmap_using_FromBitmap() { @@ -758,6 +834,25 @@ public void TestGetRGBBuffer() Assert.Equal(firstPixel.B, buffer[2]); } + //[FactWithAutomaticDisplayName] + public void TestGetRGBABuffer() + { + string imagePath = GetRelativeFilePath("checkmark.jpg"); + using var bitmap = new AnyBitmap(imagePath); + var expectedSize = bitmap.Width * bitmap.Height * 4; // 4 bytes per pixel (RGB) + + byte[] buffer = bitmap.GetRGBABuffer(); + + Assert.Equal(expectedSize, buffer.Length); + + // Verify the first pixel's RGB values + var firstPixel = bitmap.GetPixel(0, 0); + Assert.Equal(firstPixel.R, buffer[0]); + Assert.Equal(firstPixel.G, buffer[1]); + Assert.Equal(firstPixel.B, buffer[2]); + Assert.Equal(firstPixel.A, buffer[3]); + } + [FactWithAutomaticDisplayName] public void Test_LoadFromRGBBuffer() { @@ -822,10 +917,11 @@ public void AnyBitmapShouldReturnCorrectResolutions(string fileName, double expe { string imagePath = GetRelativeFilePath(fileName); var bitmap = AnyBitmap.FromFile(imagePath); + var frames = bitmap.GetAllFrames; for (int i = 0; i < bitmap.FrameCount; i++) { - Assert.Equal(expectedHorizontalResolution, bitmap.GetAllFrames.ElementAt(i).HorizontalResolution); - Assert.Equal(expectedVerticalResolution, bitmap.GetAllFrames.ElementAt(i).VerticalResolution); + Assert.Equal(expectedHorizontalResolution, frames.ElementAt(i).HorizontalResolution.Value, 1d); + Assert.Equal(expectedVerticalResolution, frames.ElementAt(i).VerticalResolution.Value, 1d); } } @@ -980,30 +1076,95 @@ public void CastAnyBitmap_from_SixLabors() #endif - [IgnoreOnAzureDevopsX86Fact] - public void Load_TiffImage_ShouldNotIncreaseFileSize() + [IgnoreOnAzureDevopsX86Fact] + public void Load_TiffImage_ShouldNotIncreaseFileSize() + { + // Arrange + #if NET6_0_OR_GREATER + double thresholdPercent = 0.15; + #else + double thresholdPercent = 1.5; + #endif + string imagePath = GetRelativeFilePath("test_dw_10.tif"); + string outputImagePath = "output.tif"; + + // Act + var bitmap = new AnyBitmap(imagePath); + bitmap.SaveAs(outputImagePath); + var originalFileSize = new FileInfo(imagePath).Length; + var maxAllowedFileSize = (long)(originalFileSize * (1 + thresholdPercent)); + var outputFileSize = new FileInfo(outputImagePath).Length; + + // Assert + outputFileSize.Should().BeLessThanOrEqualTo(maxAllowedFileSize); + + // Clean up + File.Delete(outputImagePath); + } + + [Theory] + [InlineData("DW-26 MultiPageTif120Input.tiff")] + [InlineData("google_large_1500dpi.bmp")] + public void DW_34_ShouldNotThrowOutOfMemory(string filename) { - // Arrange -#if NET6_0_OR_GREATER - double thresholdPercent = 0.15; -#else - double thresholdPercent = 1.5; -#endif - string imagePath = GetRelativeFilePath("test_dw_10.tif"); - string outputImagePath = "output.tif"; + string imagePath = GetRelativeFilePath(filename); - // Act - var bitmap = new AnyBitmap(imagePath); - bitmap.SaveAs(outputImagePath); - var originalFileSize = new FileInfo(imagePath).Length; - var maxAllowedFileSize = (long)(originalFileSize * (1 + thresholdPercent)); - var outputFileSize = new FileInfo(outputImagePath).Length; + List images = new List(); + for (int i = 0; i < 25; i++) + { + var bitmap = new AnyBitmap(imagePath); + images.Add(bitmap); + bitmap.IsImageLoaded().Should().BeFalse(); + } - // Assert - outputFileSize.Should().BeLessThanOrEqualTo(maxAllowedFileSize); + images.ForEach(bitmap => bitmap.Dispose()); + } - // Clean up - File.Delete(outputImagePath); + //[Fact] + //public void LoadTiff() + //{ + // Stopwatch stopWatch = new Stopwatch(); + // stopWatch.Start(); + // for (int i = 0; i < 25; i++) + // { + // var bitmap = new AnyBitmap("C:\\repo\\IronInternalBenchmarks\\IronOcrBenchmark\\Images\\001_20221121000002_S2123457_EL37.tiff"); + // //var c = bitmap.GetPixel(10,10); + // foreach (var item in bitmap.GetAllFrames) + // { + // item.GetRGBBuffer(); + // item.ExtractAlphaData(); + // } + + + // } + // stopWatch.Stop(); + // // Get the elapsed time as a TimeSpan value. + // TimeSpan ts = stopWatch.Elapsed; + // ts.Should().Be(TimeSpan.FromHours(1)); + //} + + // [FactWithAutomaticDisplayName] + public void AnyBitmap_ExportGif_Should_Works() + { + string imagePath = GetRelativeFilePath("van-gogh-starry-night-vincent-van-gogh.jpg"); + var anyBitmap = AnyBitmap.FromFile(imagePath); + + using var resultExport = new MemoryStream(); + anyBitmap.ExportStream(resultExport, AnyBitmap.ImageFormat.Gif); + resultExport.Length.Should().NotBe(0); + Image.DetectFormat(resultExport.ToArray()).Should().Be(SixLabors.ImageSharp.Formats.Gif.GifFormat.Instance); + } + + // [FactWithAutomaticDisplayName] + public void AnyBitmap_ExportTiff_Should_Works() + { + string imagePath = GetRelativeFilePath("van-gogh-starry-night-vincent-van-gogh.jpg"); + var anyBitmap = AnyBitmap.FromFile(imagePath); + + using var resultExport = new MemoryStream(); + anyBitmap.ExportStream(resultExport, AnyBitmap.ImageFormat.Tiff); + resultExport.Length.Should().NotBe(0); + Image.DetectFormat(resultExport.ToArray()).Should().Be(SixLabors.ImageSharp.Formats.Tiff.TiffFormat.Instance); } } diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/ColorFunctionality.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/ColorFunctionality.cs index abe8e17..554824b 100644 --- a/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/ColorFunctionality.cs +++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/ColorFunctionality.cs @@ -399,20 +399,20 @@ public void Cast_ImageSharp_Rgb24_to_Color() [FactWithAutomaticDisplayName] public void Cast_ImageSharp_Rgb48_from_Color() { - var imgColor = new SixLabors.ImageSharp.PixelFormats.Rgb48(255, 0, 0); + var imgColor = new SixLabors.ImageSharp.PixelFormats.Rgb48(65535, 0, 0); Color red = imgColor; Assert.Equal(255, red.R); Assert.Equal(0, red.G); Assert.Equal(0, red.B); - imgColor = new SixLabors.ImageSharp.PixelFormats.Rgb48(0, 255, 0); + imgColor = new SixLabors.ImageSharp.PixelFormats.Rgb48(0, 65535, 0); Color green = imgColor; Assert.Equal(255, green.A); Assert.Equal(0, green.R); Assert.Equal(255, green.G); Assert.Equal(0, green.B); - imgColor = new SixLabors.ImageSharp.PixelFormats.Rgb48(0, 0, 255); + imgColor = new SixLabors.ImageSharp.PixelFormats.Rgb48(0, 0, 65535); Color blue = imgColor; Assert.Equal(0, blue.R); Assert.Equal(0, blue.G); @@ -423,28 +423,29 @@ public void Cast_ImageSharp_Rgb48_from_Color() public void Cast_ImageSharp_Rgb48_to_Color() { Color color = Color.Red; + //Rgb42 is 16-bit color (0-65535) not (0-255) SixLabors.ImageSharp.PixelFormats.Rgb48 red = color; - Assert.Equal(255, red.R); + Assert.Equal(65535, red.R); Assert.Equal(0, red.G); Assert.Equal(0, red.B); color = new Color(0, 255, 0); SixLabors.ImageSharp.PixelFormats.Rgb48 green = color; Assert.Equal(0, green.R); - Assert.Equal(255, green.G); + Assert.Equal(65535, green.G); Assert.Equal(0, green.B); color = new Color("#0000FF"); SixLabors.ImageSharp.PixelFormats.Rgb48 blue = color; Assert.Equal(0, blue.R); Assert.Equal(0, blue.G); - Assert.Equal(255, blue.B); + Assert.Equal(65535, blue.B); color = Color.FromArgb(Convert.ToInt32("1e81b0", 16)); SixLabors.ImageSharp.PixelFormats.Rgb48 imgColor = color; - Assert.Equal(30, imgColor.R); - Assert.Equal(129, imgColor.G); - Assert.Equal(176, imgColor.B); + Assert.Equal(7710, imgColor.R); + Assert.Equal(33153, imgColor.G); + Assert.Equal(45232, imgColor.B); } [FactWithAutomaticDisplayName] @@ -456,14 +457,14 @@ public void Cast_ImageSharp_Rgba64_from_Color() Assert.Equal(0, red.G); Assert.Equal(0, red.B); - imgColor = new SixLabors.ImageSharp.PixelFormats.Rgba64(0, 255, 0, 255); + imgColor = new SixLabors.ImageSharp.PixelFormats.Rgba64(0, 65535, 0, 65535); Color green = imgColor; Assert.Equal(255, green.A); Assert.Equal(0, green.R); Assert.Equal(255, green.G); Assert.Equal(0, green.B); - imgColor = new SixLabors.ImageSharp.PixelFormats.Rgba64(0, 0, 255, 255); + imgColor = new SixLabors.ImageSharp.PixelFormats.Rgba64(0, 0, 65535, 65535); Color blue = imgColor; Assert.Equal(255, green.A); Assert.Equal(0, blue.R); diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common/AnyBitmap.Enum.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common/AnyBitmap.Enum.cs new file mode 100644 index 0000000..bc67012 --- /dev/null +++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common/AnyBitmap.Enum.cs @@ -0,0 +1,213 @@ +using System; +using System.IO; + +namespace IronSoftware.Drawing +{ +public partial class AnyBitmap + { + #pragma warning disable CS0618 + /// + /// Converts the legacy to and + /// + [Obsolete("RotateFlipType is legacy support from System.Drawing. " + + "Please use RotateMode and FlipMode instead.")] + internal static (RotateMode, FlipMode) ParseRotateFlipType(RotateFlipType rotateFlipType) + { + return rotateFlipType switch + { + RotateFlipType.RotateNoneFlipNone or RotateFlipType.Rotate180FlipXY => (RotateMode.None, FlipMode.None), + RotateFlipType.Rotate90FlipNone or RotateFlipType.Rotate270FlipXY => (RotateMode.Rotate90, FlipMode.None), + RotateFlipType.RotateNoneFlipXY or RotateFlipType.Rotate180FlipNone => (RotateMode.Rotate180, FlipMode.None), + RotateFlipType.Rotate90FlipXY or RotateFlipType.Rotate270FlipNone => (RotateMode.Rotate270, FlipMode.None), + RotateFlipType.RotateNoneFlipX or RotateFlipType.Rotate180FlipY => (RotateMode.None, FlipMode.Horizontal), + RotateFlipType.Rotate90FlipX or RotateFlipType.Rotate270FlipY => (RotateMode.Rotate90, FlipMode.Horizontal), + RotateFlipType.RotateNoneFlipY or RotateFlipType.Rotate180FlipX => (RotateMode.None, FlipMode.Vertical), + RotateFlipType.Rotate90FlipY or RotateFlipType.Rotate270FlipX => (RotateMode.Rotate90, FlipMode.Vertical), + _ => throw new ArgumentOutOfRangeException(nameof(rotateFlipType), rotateFlipType, null), + }; + } + + /// + /// Provides enumeration over how a image should be flipped. + /// + public enum FlipMode + { + /// + /// Don't flip the image. + /// + None, + + /// + /// Flip the image horizontally. + /// + Horizontal, + + /// + /// Flip the image vertically. + /// + Vertical + } + + /// + /// Popular image formats which can read and export. + /// + /// + /// + /// + public enum ImageFormat + { + /// The Bitmap image format. + Bmp = 0, + + /// The Gif image format. + Gif = 1, + + /// The Tiff image format. + Tiff = 2, + + /// The Jpeg image format. + Jpeg = 3, + + /// The PNG image format. + Png = 4, + + /// The WBMP image format. Will default to BMP if not + /// supported on the runtime platform. + Wbmp = 5, + + /// The new WebP image format. + Webp = 6, + + /// The Icon image format. + Icon = 7, + + /// The Wmf image format. + Wmf = 8, + + /// The Raw image format. + RawFormat = 9, + + /// The existing raw image format. + Default = -1 + + } + + /// + /// Specifies how much an image is rotated and the axis used to flip + /// the image. This follows the legacy System.Drawing.RotateFlipType + /// notation. + /// + [Obsolete("RotateFlipType is legacy support from System.Drawing. " + + "Please use RotateMode and FlipMode instead.")] + public enum RotateFlipType + { + /// + /// Specifies no clockwise rotation and no flipping. + /// + RotateNoneFlipNone, + /// + /// Specifies a 180-degree clockwise rotation followed by a + /// horizontal and vertical flip. + /// + Rotate180FlipXY, + + /// + /// Specifies a 90-degree clockwise rotation without flipping. + /// + Rotate90FlipNone, + /// + /// Specifies a 270-degree clockwise rotation followed by a + /// horizontal and vertical flip. + /// + Rotate270FlipXY, + + /// + /// Specifies no clockwise rotation followed by a horizontal and + /// vertical flip. + /// + RotateNoneFlipXY, + /// + /// Specifies a 180-degree clockwise rotation without flipping. + /// + Rotate180FlipNone, + + /// + /// Specifies a 90-degree clockwise rotation followed by a + /// horizontal and vertical flip. + /// + Rotate90FlipXY, + /// + /// Specifies a 270-degree clockwise rotation without flipping. + /// + Rotate270FlipNone, + + /// + /// Specifies no clockwise rotation followed by a horizontal flip. + /// + RotateNoneFlipX, + /// + /// Specifies a 180-degree clockwise rotation followed by a + /// vertical flip. + /// + Rotate180FlipY, + + /// + /// Specifies a 90-degree clockwise rotation followed by a + /// horizontal flip. + /// + Rotate90FlipX, + /// + /// Specifies a 270-degree clockwise rotation followed by a + /// vertical flip. + /// + Rotate270FlipY, + + /// + /// Specifies no clockwise rotation followed by a vertical flip. + /// + RotateNoneFlipY, + /// + /// Specifies a 180-degree clockwise rotation followed by a + /// horizontal flip. + /// + Rotate180FlipX, + + /// + /// Specifies a 90-degree clockwise rotation followed by a + /// vertical flip. + /// + Rotate90FlipY, + /// + /// Specifies a 270-degree clockwise rotation followed by a + /// horizontal flip. + /// + Rotate270FlipX + } + + /// + /// Provides enumeration over how the image should be rotated. + /// + public enum RotateMode + { + /// + /// Do not rotate the image. + /// + None, + + /// + /// Rotate the image by 90 degrees clockwise. + /// + Rotate90 = 90, + + /// + /// Rotate the image by 180 degrees clockwise. + /// + Rotate180 = 180, + + /// + /// Rotate the image by 270 degrees clockwise. + /// + Rotate270 = 270 + } + } +} \ No newline at end of file diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common/AnyBitmap.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common/AnyBitmap.cs index 6a756a6..c5f17fe 100644 --- a/IronSoftware.Drawing/IronSoftware.Drawing.Common/AnyBitmap.cs +++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common/AnyBitmap.cs @@ -15,13 +15,16 @@ using SkiaSharp; using System; using System.Collections.Generic; +using System.ComponentModel; using System.Drawing.Imaging; using System.IO; using System.Linq; using System.Net; using System.Net.Http; +using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Threading; using System.Threading.Tasks; namespace IronSoftware.Drawing @@ -48,34 +51,101 @@ public partial class AnyBitmap : IDisposable, IAnyImage { private bool _disposed = false; - private Image Image { get; set; } - private byte[] Binary { get; set; } - private IImageFormat Format { get; set; } - private TiffCompression TiffCompression { get; set; } = TiffCompression.Lzw; - private bool PreserveOriginalFormat { get; set; } = true; + /// + /// We use Lazy because in some case we can skip Image.Load (which use a lot of memory). + /// e.g. open jpg file and save it to jpg file without changing anything so we don't need to load the image. + /// + private Lazy> _lazyImage; + + private IReadOnlyList GetInternalImages() + { + return _lazyImage?.Value ?? throw new InvalidOperationException("No image data available"); + } + + private Image GetFirstInternalImage() + { + return (_lazyImage?.Value?[0]) ?? throw new InvalidOperationException("No image data available"); + } + + private void ForceLoadLazyImage() + { + var _ = _lazyImage?.Value; + } + + private readonly object _binaryLock = new object(); + private byte[] _binary; /// - /// Width of the image. + /// This value save the original bytes, we need to update it each time that Image object (inside _lazyImage) is changed /// - public int Width + private byte[] Binary { get { - return Image.Width; + + if (_binary == null) + { + //In case like Binary will be assign once the image is loaded + ForceLoadLazyImage(); //force load but _binary can still be null depended on how _lazyImage was loaded + } + + if (_binary == null || IsDirty) + { + lock (_binaryLock) + { + if (_binary == null || IsDirty) + { + //Which mean we need to update _binary to sync with the image + using var stream = new MemoryStream(); + IImageEncoder enc = GetDefaultImageExportEncoder(); + + GetFirstInternalImage().Save(stream, enc); + _binary = stream.ToArray(); + IsDirty = false; + } + } + } + + return _binary; + } + set + { + _binary = value; } } + private int _isDirty; + /// - /// Height of the image. + /// If IsDirty = true means we need to update Binary. Since Image object (inside _lazyImage) is changed /// - public int Height + private bool IsDirty { - get - { - return Image.Height; - } + // use Interlocked to make sure that it always updated and thread safe. + get => Thread.VolatileRead(ref _isDirty) == 1; + set => Interlocked.Exchange(ref _isDirty, value ? 1 : 0); } + private IImageFormat Format => Image.DetectFormat(Binary); + private TiffCompression TiffCompression { get; set; } = TiffCompression.Lzw; + private bool PreserveOriginalFormat { get; set; } = true; + + //cache since Image.Width (ImageSharp) is slow + private int? _width = null; + + /// + /// Width of the image. + /// + public int Width => _width ??= GetFirstInternalImage().Width; + + //cache since Image.Height (ImageSharp) is slow + private int? _height = null; + + /// + /// Height of the image. + /// + public int Height => _height ??= GetFirstInternalImage().Height; + /// /// Number of raw image bytes stored /// @@ -153,14 +223,8 @@ public AnyBitmap Clone() /// public AnyBitmap Clone(Rectangle rectangle) { - using Image image = Image.Clone(img => img.Crop(rectangle)); - using var memoryStream = new MemoryStream(); - image.Save(memoryStream, new BmpEncoder() - { - BitsPerPixel = BmpBitsPerPixel.Pixel32, - SupportTransparency = true - }); - return new AnyBitmap(memoryStream.ToArray()); + var cloned = GetInternalImages().Select(img => img.Clone(x => x.Crop(rectangle))); + return new AnyBitmap(Binary, cloned); } /// @@ -273,46 +337,41 @@ public void ExportStream( ImageFormat format = ImageFormat.Default, int lossy = 100) { - if (format is ImageFormat.Default or ImageFormat.RawFormat) - { - var writer = new BinaryWriter(stream); - writer.Write(Binary); - return; - } if (lossy is < 0 or > 100) { lossy = 100; } + //this Check if _lazyImage is not loaded which mean we didn't touch anything and the output format is the same then we should just return the original data + + var isSameFormat = (format is ImageFormat.Default or ImageFormat.RawFormat) || (GetImageFormat() == format); + var isCompressNeeded = (format is ImageFormat.Default or ImageFormat.Webp or ImageFormat.Jpeg) && lossy != 100; + + if (!IsDirty && isSameFormat && !isCompressNeeded) + { + var writer = new BinaryWriter(stream); + writer.Write(Binary); + return; + } + try { - IImageEncoder enc = format switch + IImageEncoder enc = GetDefaultImageExportEncoder(format, lossy); + if (enc is TiffEncoder) { - ImageFormat.Jpeg => new JpegEncoder() - { - Quality = lossy, -#if NET6_0_OR_GREATER - ColorType = JpegEncodingColor.Rgb -#else - ColorType = JpegColorType.Rgb -#endif - }, - ImageFormat.Gif => new GifEncoder(), - ImageFormat.Png => new PngEncoder(), - ImageFormat.Webp => new WebpEncoder() { Quality = lossy }, - ImageFormat.Tiff => new TiffEncoder() - { - Compression = TiffCompression - }, - _ => new BmpEncoder() - { - BitsPerPixel = BmpBitsPerPixel.Pixel32, - SupportTransparency = true - }, - }; + InternalSaveAsMultiPageTiff(_lazyImage?.Value, stream); + } + else if (enc is GifEncoder) + { + InternalSaveAsMultiPageGif(_lazyImage?.Value, stream); + + } + else + { + GetFirstInternalImage().Save(stream, enc); + } - Image.Save(stream, enc); } catch (DllNotFoundException e) { @@ -500,7 +559,7 @@ public static AnyBitmap FromBytes(byte[] bytes, bool preserveOriginalFormat) /// /// Create a new Bitmap from a (bytes). /// - /// A of image data in any common format. + /// A of image data in any common format. /// Default is true. Set to false to load as Rgba32. /// /// @@ -716,9 +775,69 @@ public AnyBitmap(Uri uri, bool preserveOriginalFormat) /// Background color of new AnyBitmap public AnyBitmap(int width, int height, Color backgroundColor = null) { - CreateNewImageInstance(width, height, backgroundColor); + _lazyImage = new Lazy>(() => + { + var image = new Image(width, height); + if (backgroundColor != null) + { + image.Mutate(context => context.Fill(backgroundColor)); + } + return [image]; + }); + ForceLoadLazyImage(); + } + + /// + /// Construct an AnyBitmap object from a buffer of RGB pixel data. + /// + /// An array of bytes representing the RGB pixel data. This should contain 3 bytes (one each for red, green, and blue) for each pixel in the image. + /// The width of the image, in pixels. + /// The height of the image, in pixels. + /// An AnyBitmap object that represents the image defined by the provided pixel data, width, and height. + internal AnyBitmap(byte[] buffer, int width, int height) + { + _lazyImage = new Lazy>(() => + { + var image = Image.LoadPixelData(buffer, width, height); + return [image]; + }); + } + + /// + /// Note: This only use for Casting It won't create new object Image + /// + /// + internal AnyBitmap(Image image) : this([image]) + { + } + + /// + /// Note: This only use for Casting It won't create new object Image + /// + /// + internal AnyBitmap(IEnumerable images) + { + _lazyImage = new Lazy>(() => + { + return [.. images]; + }); + } + + /// + /// Fastest AnyBitmap ctor + /// + /// + /// + internal AnyBitmap(byte[] bytes, IEnumerable images) + { + Binary = bytes; + _lazyImage = new Lazy>(() => + { + return [.. images]; + }); } + /// /// Create a new Bitmap from a file. /// @@ -855,29 +974,21 @@ public static AnyBitmap FromUri(Uri uri, bool preserveOriginalFormat) /// An AnyBitmap object that represents the image defined by the provided pixel data, width, and height. public static AnyBitmap LoadAnyBitmapFromRGBBuffer(byte[] buffer, int width, int height) { - using var memoryStream = new MemoryStream(); - using var image = Image.LoadPixelData(buffer, width, height); - image.Save(memoryStream, new BmpEncoder() - { - BitsPerPixel = BmpBitsPerPixel.Pixel24 - }); - return new AnyBitmap(memoryStream.ToArray()); + return new AnyBitmap(buffer, width, height); } + //cache + private int? _bitsPerPixel = null; /// /// Gets colors depth, in number of bits per pixel. ///
Further Documentation:
/// /// Code Example
///
- public int BitsPerPixel - { - get - { - return Image.PixelType.BitsPerPixel; - } - } + public int BitsPerPixel => _bitsPerPixel ??= GetFirstInternalImage().PixelType.BitsPerPixel; + //cache + private int? _frameCount = null; /// /// Returns the number of frames in our loaded Image. Each “frame” is /// a page of an image such as Tiff or Gif. All other image formats @@ -891,12 +1002,17 @@ public int FrameCount { get { - return Image.Frames.Count; + if (!_frameCount.HasValue) + { + var images = GetInternalImages(); + _frameCount = images.Count == 1 ? images[0].Frames.Count : images.Count; + } + return _frameCount.Value; } } /// - /// Returns all of the cloned frames in our loaded Image. Each "frame" + /// Returns all of the frames in our loaded Image. Each "frame" /// is a page of an image such as Tiff or Gif. All other image formats /// return an IEnumerable of length 1. ///
Further Documentation:
@@ -909,20 +1025,14 @@ public IEnumerable GetAllFrames { get { - if (FrameCount > 1) + var images = GetInternalImages(); + if (images.Count == 1) { - List images = new(); - - for (int currFrameIndex = 0; currFrameIndex < FrameCount; currFrameIndex++) - { - images.Add(Image.Frames.CloneFrame(currFrameIndex)); - } - - return images; + return ImageFrameCollectionToImages(images[0].Frames).Select(x => (AnyBitmap)x); } else { - return new List() { Clone() }; + return images.Select(x => (AnyBitmap)x); } } } @@ -939,10 +1049,8 @@ public IEnumerable GetAllFrames /// public static AnyBitmap CreateMultiFrameTiff(IEnumerable imagePaths) { - using MemoryStream stream = - CreateMultiFrameImage(CreateAnyBitmaps(imagePaths)) - ?? throw new NotSupportedException("Image could not be loaded. File format is not supported."); - _ = stream.Seek(0, SeekOrigin.Begin); + using var stream = new MemoryStream(); + InternalSaveAsMultiPageTiff(imagePaths.Select(Image.Load), stream); return FromStream(stream); } @@ -959,10 +1067,8 @@ public static AnyBitmap CreateMultiFrameTiff(IEnumerable imagePaths) /// public static AnyBitmap CreateMultiFrameTiff(IEnumerable images) { - using MemoryStream stream = - CreateMultiFrameImage(images) - ?? throw new NotSupportedException("Image could not be loaded. File format is not supported."); - _ = stream.Seek(0, SeekOrigin.Begin); + using var stream = new MemoryStream(); + InternalSaveAsMultiPageTiff(images.Select(x => (Image)x), stream); return FromStream(stream); } @@ -979,10 +1085,8 @@ public static AnyBitmap CreateMultiFrameTiff(IEnumerable images) /// public static AnyBitmap CreateMultiFrameGif(IEnumerable imagePaths) { - using MemoryStream stream = - CreateMultiFrameImage(CreateAnyBitmaps(imagePaths), ImageFormat.Gif) - ?? throw new NotSupportedException("Image could not be loaded. File format is not supported."); - _ = stream.Seek(0, SeekOrigin.Begin); + using var stream = new MemoryStream(); + InternalSaveAsMultiPageGif(imagePaths.Select(Image.Load), stream); return FromStream(stream); } @@ -999,10 +1103,8 @@ public static AnyBitmap CreateMultiFrameGif(IEnumerable imagePaths) /// public static AnyBitmap CreateMultiFrameGif(IEnumerable images) { - using MemoryStream stream = - CreateMultiFrameImage(images, ImageFormat.Gif) - ?? throw new NotSupportedException("Image could not be loaded. File format is not supported."); - _ = stream.Seek(0, SeekOrigin.Begin); + using var stream = new MemoryStream(); + InternalSaveAsMultiPageGif(images.Select(x => (Image)x), stream); return FromStream(stream); } @@ -1013,37 +1115,81 @@ public static AnyBitmap CreateMultiFrameGif(IEnumerable images) /// Thrown when the image's bit depth is not 32 bpp. public byte[] ExtractAlphaData() { - if (BitsPerPixel == 32) + + var alpha = new byte[Width * Height]; + + switch (GetFirstInternalImage()) { - var alpha = new byte[Image.Width * Image.Height]; - int alphaIndex = 0; - using var rgbaImage = Image is Image image - ? image - : Image.CloneAs(); - rgbaImage.ProcessPixelRows(accessor => - { - for (int y = 0; y < accessor.Height; y++) + case Image imageAsFormat: + imageAsFormat.ProcessPixelRows(accessor => { - // Get the row as a span of Rgba32. - Span pixelRow = accessor.GetRowSpan(y); - // Interpret the row as a span of bytes. - Span rowBytes = MemoryMarshal.AsBytes(pixelRow); - - // Each pixel is 4 bytes: R, G, B, A. - // The alpha channel is the fourth byte (index 3, 7, 11, ...). - for (int i = 3; i < rowBytes.Length; i += 4) + for (int y = 0; y < accessor.Height; y++) { - alpha[alphaIndex++] = rowBytes[i]; + Span pixelRow = accessor.GetRowSpan(y); + for (int x = 0; x < accessor.Width; x++) + { + alpha[y * accessor.Width + x] = pixelRow[x].A; + } } - } - }); - - return alpha.ToArray(); - } - else - { - throw new NotSupportedException($"Extracting alpha data is not supported for {BitsPerPixel} bpp images."); + }); + break; + case Image imageAsFormat: + imageAsFormat.ProcessPixelRows(accessor => + { + for (int y = 0; y < accessor.Height; y++) + { + Span pixelRow = accessor.GetRowSpan(y); + for (int x = 0; x < accessor.Width; x++) + { + alpha[y * accessor.Width + x] = pixelRow[x].A; + } + } + }); + break; + case Image imageAsFormat: + imageAsFormat.ProcessPixelRows(accessor => + { + for (int y = 0; y < accessor.Height; y++) + { + Span pixelRow = accessor.GetRowSpan(y); + for (int x = 0; x < accessor.Width; x++) + { + alpha[y * accessor.Width + x] = pixelRow[x].A; + } + } + }); + break; + case Image imageAsFormat: + imageAsFormat.ProcessPixelRows(accessor => + { + for (int y = 0; y < accessor.Height; y++) + { + Span pixelRow = accessor.GetRowSpan(y); + for (int x = 0; x < accessor.Width; x++) + { + alpha[y * accessor.Width + x] = pixelRow[x].A; + } + } + }); + break; + case Image imageAsFormat: + imageAsFormat.ProcessPixelRows(accessor => + { + for (int y = 0; y < accessor.Height; y++) + { + Span pixelRow = accessor.GetRowSpan(y); + for (int x = 0; x < accessor.Width; x++) + { + alpha[y * accessor.Width + x] = pixelRow[x].ToRgba32().A; + } + } + }); + break; + default: + throw new NotSupportedException($"Extracting alpha data is not supported for {BitsPerPixel} bpp images."); } + + return alpha.ToArray(); } /// @@ -1108,17 +1254,11 @@ public static AnyBitmap RotateFlip( _ => throw new NotImplementedException() }; - using var memoryStream = new MemoryStream(); - using var image = Image.Load(bitmap.ExportBytes()); + Image image = Image.Load(bitmap.Binary); image.Mutate(x => x.RotateFlip(rotateModeImgSharp, flipModeImgSharp)); - image.Save(memoryStream, new BmpEncoder() - { - BitsPerPixel = BmpBitsPerPixel.Pixel32, - SupportTransparency = true - }); - return new AnyBitmap(memoryStream.ToArray()); + return new AnyBitmap(image); } /// @@ -1148,18 +1288,14 @@ public static AnyBitmap Redact( Rectangle Rectangle, Color color) { - using var memoryStream = new MemoryStream(); - using var image = Image.Load(bitmap.ExportBytes()); + + //this casting will crate new object + Image image = Image.Load(bitmap.Binary); Rectangle rectangle = Rectangle; var brush = new SolidBrush(color); image.Mutate(ctx => ctx.Fill(brush, rectangle)); - image.Save(memoryStream, new BmpEncoder() - { - BitsPerPixel = BmpBitsPerPixel.Pixel32, - SupportTransparency = true - }); - - return new AnyBitmap(memoryStream.ToArray()); + + return new AnyBitmap(image); } /// @@ -1231,7 +1367,7 @@ public double? HorizontalResolution { get { - return Image?.Metadata.HorizontalResolution ?? null; + return GetFirstInternalImage().Metadata.HorizontalResolution; } } @@ -1243,7 +1379,7 @@ public double? VerticalResolution { get { - return Image?.Metadata.VerticalResolution ?? null; + return GetFirstInternalImage().Metadata.VerticalResolution; } } @@ -1274,8 +1410,8 @@ public Color GetPixel(int x, int y) } /// - /// Sets the of the specified pixel in this - /// + /// Sets the of the specified pixel in this + /// Performs an operation that modifies the current object. (mutable) /// Set in Rgb24 color format. /// /// The x-coordinate of the pixel to retrieve. @@ -1295,7 +1431,6 @@ public void SetPixel(int x, int y, Color color) throw new ArgumentOutOfRangeException(nameof(y), "y is less than 0, or greater than or equal to Height."); } - SetPixelColor(x, y, color); } @@ -1309,89 +1444,459 @@ public void SetPixel(int x, int y, Color color) /// public byte[] GetRGBBuffer() { - using Image image = Image.CloneAs(); - + var image = GetFirstInternalImage(); int width = image.Width; int height = image.Height; - byte[] rgbBuffer = new byte[width * height * 3]; // 3 bytes per pixel (RGB) - - image.ProcessPixelRows(accessor => + switch (image) { - for (int y = 0; y < accessor.Height; y++) - { - Span pixelRow = accessor.GetRowSpan(y); - - for (int x = 0; x < accessor.Width; x++) + case Image imageAsFormat: + imageAsFormat.ProcessPixelRows(accessor => { - ref Rgb24 pixel = ref pixelRow[x]; - - int bufferIndex = (y * width + x) * 3; - rgbBuffer[bufferIndex] = pixel.R; - rgbBuffer[bufferIndex + 1] = pixel.G; - rgbBuffer[bufferIndex + 2] = pixel.B; - } - } - }); - + for (int y = 0; y < accessor.Height; y++) + { + Span pixelRow = accessor.GetRowSpan(y); + for (int x = 0; x < width; x++) + { + Rgba32 pixel = pixelRow[x]; + int index = (y * width + x) * 3; + + rgbBuffer[index] = pixel.R; + rgbBuffer[index + 1] = pixel.G; + rgbBuffer[index + 2] = pixel.B; + } + } + }); + break; + case Image imageAsFormat: + imageAsFormat.ProcessPixelRows(accessor => + { + for (int y = 0; y < accessor.Height; y++) + { + Span pixelRow = accessor.GetRowSpan(y); + for (int x = 0; x < width; x++) + { + Rgb24 pixel = pixelRow[x]; + int index = (y * width + x) * 3; + + rgbBuffer[index] = pixel.R; + rgbBuffer[index + 1] = pixel.G; + rgbBuffer[index + 2] = pixel.B; + } + } + }); + break; + case Image imageAsFormat: + imageAsFormat.ProcessPixelRows(accessor => + { + for (int y = 0; y < accessor.Height; y++) + { + Span pixelRow = accessor.GetRowSpan(y); + for (int x = 0; x < width; x++) + { + Abgr32 pixel = pixelRow[x]; + int index = (y * width + x) * 3; + + rgbBuffer[index] = pixel.R; + rgbBuffer[index + 1] = pixel.G; + rgbBuffer[index + 2] = pixel.B; + } + } + }); + break; + case Image imageAsFormat: + imageAsFormat.ProcessPixelRows(accessor => + { + for (int y = 0; y < accessor.Height; y++) + { + Span pixelRow = accessor.GetRowSpan(y); + for (int x = 0; x < width; x++) + { + Argb32 pixel = pixelRow[x]; + int index = (y * width + x) * 3; + + rgbBuffer[index] = pixel.R; + rgbBuffer[index + 1] = pixel.G; + rgbBuffer[index + 2] = pixel.B; + } + } + }); + break; + case Image imageAsFormat: + imageAsFormat.ProcessPixelRows(accessor => + { + for (int y = 0; y < accessor.Height; y++) + { + Span pixelRow = accessor.GetRowSpan(y); + for (int x = 0; x < width; x++) + { + Bgr24 pixel = pixelRow[x]; + int index = (y * width + x) * 3; + + rgbBuffer[index] = pixel.R; + rgbBuffer[index + 1] = pixel.G; + rgbBuffer[index + 2] = pixel.B; + } + } + }); + break; + case Image imageAsFormat: + imageAsFormat.ProcessPixelRows(accessor => + { + for (int y = 0; y < accessor.Height; y++) + { + Span pixelRow = accessor.GetRowSpan(y); + for (int x = 0; x < width; x++) + { + Bgra32 pixel = pixelRow[x]; + int index = (y * width + x) * 3; + + rgbBuffer[index] = pixel.R; + rgbBuffer[index + 1] = pixel.G; + rgbBuffer[index + 2] = pixel.B; + } + } + }); + break; + case Image imageAsFormat: + imageAsFormat.ProcessPixelRows(accessor => + { + for (int y = 0; y < accessor.Height; y++) + { + Span pixelRow = accessor.GetRowSpan(y); + for (int x = 0; x < width; x++) + { + //required casting in 16bit color + Color pixel = (Color)pixelRow[x]; + int index = (y * width + x) * 3; + + rgbBuffer[index] = pixel.R; + rgbBuffer[index + 1] = pixel.G; + rgbBuffer[index + 2] = pixel.B; + } + } + }); + break; + case Image imageAsFormat: + imageAsFormat.ProcessPixelRows(accessor => + { + for (int y = 0; y < accessor.Height; y++) + { + Span pixelRow = accessor.GetRowSpan(y); + for (int x = 0; x < width; x++) + { + //required casting in 16bit color + Color pixel = (Color)pixelRow[x]; + int index = (y * width + x) * 3; + + rgbBuffer[index] = pixel.R; + rgbBuffer[index + 1] = pixel.G; + rgbBuffer[index + 2] = pixel.B; + } + } + }); + break; + default: + var clonedImage = image.CloneAs(); + clonedImage.ProcessPixelRows(accessor => + { + for (int y = 0; y < accessor.Height; y++) + { + Span pixelRow = accessor.GetRowSpan(y); + for (int x = 0; x < width; x++) + { + Rgb24 pixel = pixelRow[x]; + int index = (y * width + x) * 3; + + rgbBuffer[index] = pixel.R; + rgbBuffer[index + 1] = pixel.G; + rgbBuffer[index + 2] = pixel.B; + } + } + }); + clonedImage.Dispose(); + break; + } return rgbBuffer; } - #region Implicit Casting - /// - /// Implicitly casts SixLabors.ImageSharp.Image objects to - /// . - /// When your .NET Class methods use as - /// parameters or return types, you now automatically support ImageSharp - /// as well. - /// When casting to and from AnyBitmap, - /// please remember to dispose your original SixLabors.ImageSharp.Image object - /// to avoid unnecessary memory allocation. + /// Retrieves the RGBA buffer from the image at the specified path. /// - /// SixLabors.ImageSharp.Image will automatically - /// be casted to . - public static implicit operator AnyBitmap(Image image) + /// An array of bytes representing the RGBA buffer of the image. + /// + /// Each pixel is represented by four bytes in the order: red, green, blue, alpha. + /// The pixels are read from the image row by row, from top to bottom and left to right within each row. + /// + public byte[] GetRGBABuffer() { - try - { - using var memoryStream = new MemoryStream(); - image.Save(memoryStream, new BmpEncoder() - { - BitsPerPixel = BmpBitsPerPixel.Pixel24 - }); - return new AnyBitmap(memoryStream.ToArray()); - - } - catch (DllNotFoundException e) - { - throw new DllNotFoundException( - "Please install SixLabors.ImageSharp from NuGet.", e); - } - catch (Exception e) + var image = GetFirstInternalImage(); + int width = image.Width; + int height = image.Height; + byte[] rgbBuffer = new byte[width * height * 4]; // 3 bytes per pixel (RGB) + switch (image) { - throw new NotSupportedException( - "Error while casting AnyBitmap from SixLabors.ImageSharp.Image", e); - } + case Image imageAsFormat: + imageAsFormat.ProcessPixelRows(accessor => + { + for (int y = 0; y < accessor.Height; y++) + { + Span pixelRow = accessor.GetRowSpan(y); + for (int x = 0; x < width; x++) + { + Rgba32 pixel = pixelRow[x]; + int index = (y * width + x) * 4; + + rgbBuffer[index] = pixel.R; + rgbBuffer[index + 1] = pixel.G; + rgbBuffer[index + 2] = pixel.B; + rgbBuffer[index + 3] = pixel.A; + } + } + }); + break; + case Image imageAsFormat: + imageAsFormat.ProcessPixelRows(accessor => + { + for (int y = 0; y < accessor.Height; y++) + { + Span pixelRow = accessor.GetRowSpan(y); + for (int x = 0; x < width; x++) + { + Rgb24 pixel = pixelRow[x]; + int index = (y * width + x) * 4; + + rgbBuffer[index] = pixel.R; + rgbBuffer[index + 1] = pixel.G; + rgbBuffer[index + 2] = pixel.B; + rgbBuffer[index + 3] = byte.MaxValue; + } + } + }); + break; + case Image imageAsFormat: + imageAsFormat.ProcessPixelRows(accessor => + { + for (int y = 0; y < accessor.Height; y++) + { + Span pixelRow = accessor.GetRowSpan(y); + for (int x = 0; x < width; x++) + { + Abgr32 pixel = pixelRow[x]; + int index = (y * width + x) * 4; + + rgbBuffer[index] = pixel.R; + rgbBuffer[index + 1] = pixel.G; + rgbBuffer[index + 2] = pixel.B; + rgbBuffer[index + 3] = pixel.A; + } + } + }); + break; + case Image imageAsFormat: + imageAsFormat.ProcessPixelRows(accessor => + { + for (int y = 0; y < accessor.Height; y++) + { + Span pixelRow = accessor.GetRowSpan(y); + for (int x = 0; x < width; x++) + { + Argb32 pixel = pixelRow[x]; + int index = (y * width + x) * 4; + + rgbBuffer[index] = pixel.R; + rgbBuffer[index + 1] = pixel.G; + rgbBuffer[index + 2] = pixel.B; + rgbBuffer[index + 3] = pixel.A; + } + } + }); + break; + case Image imageAsFormat: + imageAsFormat.ProcessPixelRows(accessor => + { + for (int y = 0; y < accessor.Height; y++) + { + Span pixelRow = accessor.GetRowSpan(y); + for (int x = 0; x < width; x++) + { + Bgr24 pixel = pixelRow[x]; + int index = (y * width + x) * 4; + + rgbBuffer[index] = pixel.R; + rgbBuffer[index + 1] = pixel.G; + rgbBuffer[index + 2] = pixel.B; + rgbBuffer[index + 3] = byte.MaxValue; + } + } + }); + break; + case Image imageAsFormat: + imageAsFormat.ProcessPixelRows(accessor => + { + for (int y = 0; y < accessor.Height; y++) + { + Span pixelRow = accessor.GetRowSpan(y); + for (int x = 0; x < width; x++) + { + Bgra32 pixel = pixelRow[x]; + int index = (y * width + x) * 4; + + rgbBuffer[index] = pixel.R; + rgbBuffer[index + 1] = pixel.G; + rgbBuffer[index + 2] = pixel.B; + rgbBuffer[index + 3] = pixel.A; + } + } + }); + break; + case Image imageAsFormat: + imageAsFormat.ProcessPixelRows(accessor => + { + for (int y = 0; y < accessor.Height; y++) + { + Span pixelRow = accessor.GetRowSpan(y); + for (int x = 0; x < width; x++) + { + //required casting in 16bit color + Color pixel = (Color)pixelRow[x]; + int index = (y * width + x) * 4; + + rgbBuffer[index] = pixel.R; + rgbBuffer[index + 1] = pixel.G; + rgbBuffer[index + 2] = pixel.B; + rgbBuffer[index + 3] = pixel.A; + } + } + }); + break; + case Image imageAsFormat: + imageAsFormat.ProcessPixelRows(accessor => + { + for (int y = 0; y < accessor.Height; y++) + { + Span pixelRow = accessor.GetRowSpan(y); + for (int x = 0; x < width; x++) + { + //required casting in 16bit color + Color pixel = (Color)pixelRow[x]; + int index = (y * width + x) * 4; + + rgbBuffer[index] = pixel.R; + rgbBuffer[index + 1] = pixel.G; + rgbBuffer[index + 2] = pixel.B; + rgbBuffer[index + 3] = pixel.A; + } + } + }); + break; + default: + using (var clonedImage = image.CloneAs()) + { + clonedImage.ProcessPixelRows(accessor => + { + for (int y = 0; y < accessor.Height; y++) + { + Span pixelRow = accessor.GetRowSpan(y); + for (int x = 0; x < width; x++) + { + Rgba32 pixel = pixelRow[x]; + int index = (y * width + x) * 4; + + rgbBuffer[index] = pixel.R; + rgbBuffer[index + 1] = pixel.G; + rgbBuffer[index + 2] = pixel.B; + rgbBuffer[index + 3] = pixel.A; + } + } + }); + } + break; + } + return rgbBuffer; } + #region Implicit Casting + /// - /// Implicitly casts to SixLabors.ImageSharp.Image objects from + /// Implicitly casts SixLabors.ImageSharp.Image objects to /// . - /// When your .NET Class methods use - /// as parameters or return types, you now automatically support - /// ImageSharp as well. + /// When your .NET Class methods use as + /// parameters or return types, you now automatically support ImageSharp + /// as well. /// When casting to and from AnyBitmap, - /// please remember to dispose your original IronSoftware.Drawing.AnyBitmap object + /// please remember to dispose your original SixLabors.ImageSharp.Image object /// to avoid unnecessary memory allocation. /// - /// is implicitly cast to - /// a SixLabors.ImageSharp.Image. - public static implicit operator Image(AnyBitmap bitmap) + /// SixLabors.ImageSharp.Image will automatically + /// be casted to . + public static implicit operator AnyBitmap(Image image) { try { - return Image.Load(bitmap.Binary); + return new AnyBitmap(image); + + } + catch (DllNotFoundException e) + { + throw new DllNotFoundException( + "Please install SixLabors.ImageSharp from NuGet.", e); + } + catch (Exception e) + { + throw new NotSupportedException( + "Error while casting AnyBitmap from SixLabors.ImageSharp.Image", e); + } + } + + /// + /// 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) + { + try + { + if (!bitmap.IsImageLoaded()) + { + var format = bitmap.Format; + if (format is not TiffFormat && format is not GifFormat) + { + return Image.Load(bitmap.Binary); + } + } + + //if it is loaded or gif/tiff + var images = bitmap.GetInternalImages(); + if (images.Count == 1) + { + //not gif/tiff + return images[0]; + } + else + { + //for gif/tiff we need to resize all frame + //Tiff can have different frame size but ImageSharp does not support + var resultImage = images[0].Clone((_) => { }); + + foreach (var frame in images.Skip(1)) + { + var newFrame = frame.Clone(x => + { + x.Resize(new ResizeOptions + { + Size = new Size(resultImage.Width, resultImage.Height), + Mode = ResizeMode.BoxPad, // Pad to fit the target dimensions + PadColor = Color.Transparent, // Use transparent padding + Position = AnchorPositionMode.Center // Center the image within the frame + }); + }); + + resultImage.Frames.AddFrame(newFrame.Frames.RootFrame); + } + + return resultImage; + } } catch (DllNotFoundException e) { @@ -1405,6 +1910,88 @@ public static implicit operator Image(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 + { + try + { + if (!bitmap.IsImageLoaded()) + { + var format = bitmap.Format; + if (format is not TiffFormat && format is not GifFormat) + { + return Image.Load(bitmap.Binary); + } + + } + + var images = bitmap.GetInternalImages(); + if (images.Count == 1) + { + if (images[0] is Image correctType) + { + return correctType; + } + else + { + return images[0].CloneAs(); + } + } + else + { + var resultImage = images[0].CloneAs(); + + //for gif/tiff we need to resize all frame + //Tiff can have different frame size but ImageSharp does not support + foreach (var frame in images.Skip(1)) + { + var newFrame = frame.CloneAs(); + + newFrame.Mutate(x => x.Resize(new ResizeOptions + { + Size = new Size(resultImage.Width, resultImage.Height), + Mode = ResizeMode.BoxPad, // Pad to fit the target dimensions + PadColor = Color.Transparent, // Use transparent padding + Position = AnchorPositionMode.Center // Center the image within the frame + })); + resultImage.Frames.AddFrame(newFrame.Frames.RootFrame); + } + + return resultImage; + } + } + catch (DllNotFoundException e) + { + throw new DllNotFoundException( + "Please install SixLabors.ImageSharp from NuGet.", e); + } + catch (Exception e) + { + throw new NotSupportedException( + "Error while casting AnyBitmap to SixLabors.ImageSharp.Image", e); + } + } + + /// + /// Implicitly casts to SixLabors.ImageSharp.Image objects from + /// . + /// When your .NET Class methods use + /// as parameters or return types, you now automatically support + /// ImageSharp as well. + /// When casting to and from AnyBitmap, + /// please remember to dispose your original IronSoftware.Drawing.AnyBitmap object + /// to avoid unnecessary memory allocation. + /// + /// is implicitly cast to + /// a SixLabors.ImageSharp.Image. + public static implicit operator Image(AnyBitmap bitmap) + { + return CastToImageSharp(bitmap); + } + /// /// Implicitly casts SixLabors.ImageSharp.Image objects to /// . @@ -1421,13 +2008,7 @@ public static implicit operator AnyBitmap(Image image) { try { - using var memoryStream = new MemoryStream(); - image.Save(memoryStream, new BmpEncoder() - { - BitsPerPixel = BmpBitsPerPixel.Pixel32, - SupportTransparency = true - }); - return new AnyBitmap(memoryStream.ToArray()); + return new AnyBitmap(image); } catch (DllNotFoundException e) { @@ -1457,7 +2038,7 @@ public static implicit operator Image(AnyBitmap bitmap) { try { - return Image.Load(bitmap.Binary); + return CastToImageSharp(bitmap); } catch (DllNotFoundException e) { @@ -1487,13 +2068,7 @@ public static implicit operator AnyBitmap(Image image) { try { - using var memoryStream = new MemoryStream(); - image.Save(memoryStream, new BmpEncoder() - { - BitsPerPixel = BmpBitsPerPixel.Pixel32, - SupportTransparency = true - }); - return new AnyBitmap(memoryStream.ToArray()); + return new AnyBitmap(image); } catch (DllNotFoundException e) { @@ -1523,7 +2098,7 @@ public static implicit operator Image(AnyBitmap bitmap) { try { - return Image.Load(bitmap.Binary); + return CastToImageSharp(bitmap); } catch (DllNotFoundException e) { @@ -1825,7 +2400,7 @@ public static implicit operator System.Drawing.Bitmap(AnyBitmap bitmap) { try { - return (System.Drawing.Bitmap)System.Drawing.Image.FromStream(new MemoryStream(bitmap.Binary)); + return (System.Drawing.Bitmap)System.Drawing.Image.FromStream(bitmap.GetStream()); } catch (DllNotFoundException e) { @@ -1922,7 +2497,7 @@ public static implicit operator System.Drawing.Image(AnyBitmap bitmap) { try { - return System.Drawing.Image.FromStream(new MemoryStream(bitmap.Binary)); + return System.Drawing.Image.FromStream(bitmap.GetStream()); } catch (DllNotFoundException e) { @@ -1943,214 +2518,7 @@ public static implicit operator System.Drawing.Image(AnyBitmap bitmap) throw new Exception(e.Message, e); } } - #endregion - #region Enum Classes - - /// - /// Popular image formats which can read and export. - /// - /// - /// - /// - public enum ImageFormat - { - /// The Bitmap image format. - Bmp = 0, - - /// The Gif image format. - Gif = 1, - - /// The Tiff image format. - Tiff = 2, - - /// The Jpeg image format. - Jpeg = 3, - - /// The PNG image format. - Png = 4, - - /// The WBMP image format. Will default to BMP if not - /// supported on the runtime platform. - Wbmp = 5, - - /// The new WebP image format. - Webp = 6, - - /// The Icon image format. - Icon = 7, - - /// The Wmf image format. - Wmf = 8, - - /// The Raw image format. - RawFormat = 9, - - /// The existing raw image format. - Default = -1 - - } -# pragma warning disable CS0618 - /// - /// Converts the legacy to and - /// - [Obsolete("RotateFlipType is legacy support from System.Drawing. " + - "Please use RotateMode and FlipMode instead.")] - internal static (RotateMode, FlipMode) ParseRotateFlipType(RotateFlipType rotateFlipType) - { - return rotateFlipType switch - { - RotateFlipType.RotateNoneFlipNone or RotateFlipType.Rotate180FlipXY => (RotateMode.None, FlipMode.None), - RotateFlipType.Rotate90FlipNone or RotateFlipType.Rotate270FlipXY => (RotateMode.Rotate90, FlipMode.None), - RotateFlipType.RotateNoneFlipXY or RotateFlipType.Rotate180FlipNone => (RotateMode.Rotate180, FlipMode.None), - RotateFlipType.Rotate90FlipXY or RotateFlipType.Rotate270FlipNone => (RotateMode.Rotate270, FlipMode.None), - RotateFlipType.RotateNoneFlipX or RotateFlipType.Rotate180FlipY => (RotateMode.None, FlipMode.Horizontal), - RotateFlipType.Rotate90FlipX or RotateFlipType.Rotate270FlipY => (RotateMode.Rotate90, FlipMode.Horizontal), - RotateFlipType.RotateNoneFlipY or RotateFlipType.Rotate180FlipX => (RotateMode.None, FlipMode.Vertical), - RotateFlipType.Rotate90FlipY or RotateFlipType.Rotate270FlipX => (RotateMode.Rotate90, FlipMode.Vertical), - _ => throw new ArgumentOutOfRangeException(nameof(rotateFlipType), rotateFlipType, null), - }; - } -# pragma warning restore CS0618 - - /// - /// Provides enumeration over how the image should be rotated. - /// - public enum RotateMode - { - /// - /// Do not rotate the image. - /// - None, - - /// - /// Rotate the image by 90 degrees clockwise. - /// - Rotate90 = 90, - - /// - /// Rotate the image by 180 degrees clockwise. - /// - Rotate180 = 180, - - /// - /// Rotate the image by 270 degrees clockwise. - /// - Rotate270 = 270 - } - - /// - /// Provides enumeration over how a image should be flipped. - /// - public enum FlipMode - { - /// - /// Don't flip the image. - /// - None, - - /// - /// Flip the image horizontally. - /// - Horizontal, - - /// - /// Flip the image vertically. - /// - Vertical - } - - /// - /// Specifies how much an image is rotated and the axis used to flip - /// the image. This follows the legacy System.Drawing.RotateFlipType - /// notation. - /// - [Obsolete("RotateFlipType is legacy support from System.Drawing. " + - "Please use RotateMode and FlipMode instead.")] - public enum RotateFlipType - { - /// - /// Specifies no clockwise rotation and no flipping. - /// - RotateNoneFlipNone, - /// - /// Specifies a 180-degree clockwise rotation followed by a - /// horizontal and vertical flip. - /// - Rotate180FlipXY, - - /// - /// Specifies a 90-degree clockwise rotation without flipping. - /// - Rotate90FlipNone, - /// - /// Specifies a 270-degree clockwise rotation followed by a - /// horizontal and vertical flip. - /// - Rotate270FlipXY, - - /// - /// Specifies no clockwise rotation followed by a horizontal and - /// vertical flip. - /// - RotateNoneFlipXY, - /// - /// Specifies a 180-degree clockwise rotation without flipping. - /// - Rotate180FlipNone, - - /// - /// Specifies a 90-degree clockwise rotation followed by a - /// horizontal and vertical flip. - /// - Rotate90FlipXY, - /// - /// Specifies a 270-degree clockwise rotation without flipping. - /// - Rotate270FlipNone, - - /// - /// Specifies no clockwise rotation followed by a horizontal flip. - /// - RotateNoneFlipX, - /// - /// Specifies a 180-degree clockwise rotation followed by a - /// vertical flip. - /// - Rotate180FlipY, - - /// - /// Specifies a 90-degree clockwise rotation followed by a - /// horizontal flip. - /// - Rotate90FlipX, - /// - /// Specifies a 270-degree clockwise rotation followed by a - /// vertical flip. - /// - Rotate270FlipY, - - /// - /// Specifies no clockwise rotation followed by a vertical flip. - /// - RotateNoneFlipY, - /// - /// Specifies a 180-degree clockwise rotation followed by a - /// horizontal flip. - /// - Rotate180FlipX, - - /// - /// Specifies a 90-degree clockwise rotation followed by a - /// vertical flip. - /// - Rotate90FlipY, - /// - /// Specifies a 270-degree clockwise rotation followed by a - /// horizontal flip. - /// - Rotate270FlipX - } #endregion /// @@ -2181,9 +2549,14 @@ protected virtual void Dispose(bool disposing) { return; } - - Image?.Dispose(); - Image = null; + if (IsImageLoaded()) + { + foreach (var x in GetInternalImages() ?? []) + { + x.Dispose(); + } + } + _lazyImage = null; Binary = null; _disposed = true; } @@ -2191,93 +2564,76 @@ protected virtual void Dispose(bool disposing) #region Private Method - private void CreateNewImageInstance(int width, int height, Color backgroundColor) + private void LoadImage(Stream stream, bool preserveOriginalFormat) { - Image = new Image(width, height); - if (backgroundColor != null) + // Optimization 1: If the stream is already a MemoryStream, we can get its + // underlying array directly, avoiding a full copy cycle. + if (stream is MemoryStream memoryStream) { - Image.Mutate(context => context.Fill(backgroundColor)); + LoadImage(memoryStream.ToArray(), preserveOriginalFormat); + return; } - using var stream = new MemoryStream(); - Image.SaveAsBmp(stream); - Binary = stream.ToArray(); - } - - private void LoadImage(ReadOnlySpan bytes, bool preserveOriginalFormat) - { - Format = Image.DetectFormat(bytes); - try + + // Optimization 2: If the stream can report its length (like a FileStream), + // we can create a MemoryStream with the exact capacity needed. This avoids + // multiple buffer re-allocations as the MemoryStream grows. + if (stream.CanSeek) { - if (Format is TiffFormat) - OpenTiffToImageSharp(bytes); - else + // Ensure we read from the beginning of the stream. + stream.Position = 0; + using (var ms = new MemoryStream((int)stream.Length)) { - Binary = bytes.ToArray(); - - if (preserveOriginalFormat) - Image = Image.Load(bytes); - else - { - PreserveOriginalFormat = preserveOriginalFormat; - Image = Image.Load(bytes); + stream.CopyTo(ms, 16 * 1024); + LoadImage(ms.ToArray(), preserveOriginalFormat); + return; + } + } - // .png image pre-processing - if (Format.Name == "PNG") - Image.Mutate(img => img.BackgroundColor(SixLabors.ImageSharp.Color.White)); - } + // Fallback for non-seekable streams (e.g., a network stream). + // This is the most memory-intensive path, but necessary for this stream type. + // We use CopyTo for a cleaner implementation of the original logic. + using (var ms = new MemoryStream()) + { + stream.CopyTo(ms, 16 * 1024); + LoadImage(ms.ToArray(), preserveOriginalFormat); + } - // Fix if the input image is auto-rotated; this issue is acknowledged by SixLabors.ImageSharp community - // ref: https://github.com/SixLabors/ImageSharp/discussions/2685 - Image.Mutate(x => x.AutoOrient()); - var resolutionUnit = this.Image.Metadata.ResolutionUnits; - var horizontal = this.Image.Metadata.HorizontalResolution; - var vertical = this.Image.Metadata.VerticalResolution; + } - // Check if image metadata is accurate already - switch (resolutionUnit) - { - case SixLabors.ImageSharp.Metadata.PixelResolutionUnit.PixelsPerMeter: - // Convert metadata of the resolution unit to pixel per inch to match the conversion below of 1 meter = 37.3701 inches - this.Image.Metadata.ResolutionUnits = SixLabors.ImageSharp.Metadata.PixelResolutionUnit.PixelsPerInch; - this.Image.Metadata.HorizontalResolution = Math.Ceiling(horizontal / 39.3701); - this.Image.Metadata.VerticalResolution = Math.Ceiling(vertical / 39.3701); - break; - case SixLabors.ImageSharp.Metadata.PixelResolutionUnit.PixelsPerCentimeter: - // Convert metadata of the resolution unit to pixel per inch to match the conversion below of 1 inch = 2.54 centimeters - this.Image.Metadata.ResolutionUnits = SixLabors.ImageSharp.Metadata.PixelResolutionUnit.PixelsPerInch; - this.Image.Metadata.HorizontalResolution = Math.Ceiling(horizontal * 2.54); - this.Image.Metadata.VerticalResolution = Math.Ceiling(vertical * 2.54); - break; - default: - // No changes required due to teh metadata are accurate already - break; - } - } - } - catch (DllNotFoundException e) + /// + /// Master LoadImage method + /// + /// + /// + private void LoadImage(ReadOnlySpan span, bool preserveOriginalFormat) + { + Binary = span.ToArray(); + if (Format is TiffFormat) { - throw new DllNotFoundException( - "Please install SixLabors.ImageSharp from NuGet.", e); + if(GetTiffFrameCountFast() > 1) + { + _lazyImage = OpenTiffToImageSharp(); + } + else + { + // ImageSharp can load some single frame tiff, if failed we try again with LibTiff + _lazyImage = OpenImageToImageSharp(preserveOriginalFormat, tryWithLibTiff : true); + } + } - catch (Exception e) + else { - throw new NotSupportedException( - "Image could not be loaded. File format is not supported.", e); + _lazyImage = OpenImageToImageSharp(preserveOriginalFormat); } } - private void LoadImage(Stream stream, bool preserveOriginalFormat) + private IEnumerable ImageFrameCollectionToImages(ImageFrameCollection imageFrames) { - byte[] buffer = new byte[16 * 1024]; - using MemoryStream ms = new(); - int read; - while ((read = stream.Read(buffer, 0, buffer.Length)) > 0) + for (int i = 0; i < imageFrames.Count; i++) { - ms.Write(buffer, 0, read); + yield return imageFrames.CloneFrame(i); } - - LoadImage(ms.ToArray(), preserveOriginalFormat); } private static AnyBitmap LoadSVGImage(string file, bool preserveOriginalFormat) @@ -2448,112 +2804,172 @@ public override void WarningHandlerExt(Tiff tif, object clientData, string metho } } - private void OpenTiffToImageSharp(ReadOnlySpan bytes) + private int GetTiffFrameCountFast() { try { - int imageWidth = 0; - int imageHeight = 0; - double imageXResolution = 0; - double imageYResolution = 0; - List images = new(); - - // create a memory stream out of them - using MemoryStream tiffStream = new(bytes.ToArray()); + using var tiffStream = new MemoryStream(Binary); - // Disable warning messages + // Disable error messages for fast check Tiff.SetErrorHandler(new DisableErrorHandler()); - // open a TIFF stored in the stream - using (Tiff tiff = Tiff.ClientOpen("in-memory", "r", tiffStream, new TiffStream())) + using var tiff = Tiff.ClientOpen("in-memory", "r", tiffStream, new TiffStream()); + if (tiff == null) return 1; // Default to single frame if can't read + + return tiff.NumberOfDirectories(); + } + catch + { + return 1; // Default to single frame on any error + } + } + + private Lazy> OpenTiffToImageSharp() + { + return new Lazy>(() => + { + try + { + return InternalLoadTiff(); + } + catch (DllNotFoundException e) + { + throw new DllNotFoundException("Please install BitMiracle.LibTiff.NET from NuGet.", e); + } + catch (Exception e) { - SetTiffCompression(tiff); + throw new NotSupportedException("Error while reading TIFF image format.", e); + } + }); + } - short num = tiff.NumberOfDirectories(); - for (short i = 0; i < num; i++) - { - _ = tiff.SetDirectory(i); + private IReadOnlyList InternalLoadTiff() + { + int imageWidth = 0; + int imageHeight = 0; + double imageXResolution = 0; + double imageYResolution = 0; + //IEnumerable images = new(); - if (IsThumbnail(tiff)) - { - continue; - } + // create a memory stream out of them + using MemoryStream tiffStream = new(Binary); - var (width, height, horizontalResolution, verticalResolution) = SetWidthHeight(tiff, i, ref imageWidth, ref imageHeight, ref imageXResolution, ref imageYResolution); + // Disable warning messages + Tiff.SetErrorHandler(new DisableErrorHandler()); + List images = new(); + // open a TIFF stored in the stream + using (Tiff tiff = Tiff.ClientOpen("in-memory", "r", tiffStream, new TiffStream())) + { + SetTiffCompression(tiff); - // Read the image into the memory buffer - int[] raster = new int[height * width]; - if (!tiff.ReadRGBAImage(width, height, raster)) - { - throw new NotSupportedException("Could not read image"); - } + short num = tiff.NumberOfDirectories(); + for (short i = 0; i < num; i++) + { + _ = tiff.SetDirectory(i); - using Image bmp = new(width, height); + if (IsThumbnail(tiff)) + { + continue; + } - var bits = PrepareByteArray(bmp, raster, width, height); + var (width, height, horizontalResolution, verticalResolution) = SetWidthHeight(tiff, i, ref imageWidth, ref imageHeight, ref imageXResolution, ref imageYResolution); - images.Add(Image.LoadPixelData(bits, bmp.Width, bmp.Height)); - - // Update the metadata for image resolutions - images[0].Metadata.HorizontalResolution = horizontalResolution; - images[0].Metadata.VerticalResolution = verticalResolution; + // Read the image into the memory buffer + int[] raster = new int[height * width]; + if (!tiff.ReadRGBAImage(width, height, raster)) + { + throw new NotSupportedException("Could not read image"); } - } + var bits = PrepareByteArray(raster, width, height, 32); + + var image = Image.LoadPixelData(bits, width, height); + image.Metadata.HorizontalResolution = horizontalResolution; + image.Metadata.VerticalResolution = verticalResolution; + images.Add(image); - // find max - FindMaxWidthAndHeight(images, out int maxWidth, out int maxHeight); + //Note1: it might be some case that the bytes of current Image is smaller/bigger than the original tiff + //Note2: 'yield return' make it super slow + } - // mute first image - images[0].Mutate(img => img.Resize(new ResizeOptions - { - Size = new Size(maxWidth, maxHeight), - Mode = ResizeMode.BoxPad, - PadColor = SixLabors.ImageSharp.Color.Transparent - })); + } + return images; + } - // iterate through images past the first - for (int i = 1; i < images.Count; i++) + private Lazy> OpenImageToImageSharp(bool preserveOriginalFormat, bool tryWithLibTiff = false) + { + return new Lazy>(() => + { + try { - // mute image - images[i].Mutate(img => img.Resize(new ResizeOptions + Image img; + if (preserveOriginalFormat) { - Size = new Size(maxWidth, maxHeight), - Mode = ResizeMode.BoxPad, - PadColor = SixLabors.ImageSharp.Color.Transparent - })); + img = Image.Load(Binary); + } + else + { + PreserveOriginalFormat = preserveOriginalFormat; + img = Image.Load(Binary); + if (Format.Name == "PNG") + img.Mutate(img => img.BackgroundColor(SixLabors.ImageSharp.Color.White)); + } - // add frames to first image - _ = images[0].Frames.AddFrame(images[i].Frames.RootFrame); + CorrectImageSharp(img); - // dispose images past the first - images[i].Dispose(); - } + return [img]; + } + catch (DllNotFoundException e) + { + throw new DllNotFoundException( + "Please install SixLabors.ImageSharp from NuGet.", e); + } + catch (Exception e) + { + return tryWithLibTiff + ? InternalLoadTiff() + : throw new NotSupportedException( + "Image could not be loaded. File format is not supported.", e); + } + }); + } - // get raw binary - using var memoryStream = new MemoryStream(); - images[0].Save(memoryStream, new TiffEncoder()); - memoryStream.Seek(0, SeekOrigin.Begin); + private void CorrectImageSharp(Image img) + { - // store result - Binary = memoryStream.ToArray(); - Image?.Dispose(); - Image = images[0]; - } - catch (DllNotFoundException e) - { - throw new DllNotFoundException("Please install BitMiracle.LibTiff.NET from NuGet.", e); - } - catch (Exception e) + // Fix if the input image is auto-rotated; this issue is acknowledged by SixLabors.ImageSharp community + // ref: https://github.com/SixLabors/ImageSharp/discussions/2685 + img.Mutate(x => x.AutoOrient()); + + var resolutionUnit = img.Metadata.ResolutionUnits; + var horizontal = img.Metadata.HorizontalResolution; + var vertical = img.Metadata.VerticalResolution; + + // Check if image metadata is accurate already + switch (resolutionUnit) { - throw new NotSupportedException("Error while reading TIFF image format.", e); + case SixLabors.ImageSharp.Metadata.PixelResolutionUnit.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.HorizontalResolution = Math.Ceiling(horizontal / 39.3701); + img.Metadata.VerticalResolution = Math.Ceiling(vertical / 39.3701); + break; + case SixLabors.ImageSharp.Metadata.PixelResolutionUnit.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.HorizontalResolution = Math.Ceiling(horizontal * 2.54); + img.Metadata.VerticalResolution = Math.Ceiling(vertical * 2.54); + break; + default: + // No changes required due to teh metadata are accurate already + break; } } private void SetTiffCompression(Tiff tiff) { - Compression tiffCompression = tiff.GetField(TiffTag.COMPRESSION) != null && tiff.GetField(TiffTag.COMPRESSION).Length > 0 + Compression tiffCompression = tiff.GetField(TiffTag.COMPRESSION) != null && tiff.GetField(TiffTag.COMPRESSION).Length > 0 ? (Compression)tiff.GetField(TiffTag.COMPRESSION)[0].ToInt() : Compression.NONE; @@ -2584,13 +3000,14 @@ private bool IsThumbnail(Tiff tiff) // Current thumbnail identification relies on the SUBFILETYPE tag with a value of FileType.REDUCEDIMAGE. // This may need refinement in the future to include additional checks // (e.g., FileType.COMPRESSION is NONE, Image Dimensions). - return subFileTypeFieldValue != null && subFileTypeFieldValue.Length > 0 + return subFileTypeFieldValue != null && subFileTypeFieldValue.Length > 0 && (FileType)subFileTypeFieldValue[0].Value == FileType.REDUCEDIMAGE; } - private ReadOnlySpan PrepareByteArray(Image bmp, int[] raster, int width, int height) + private ReadOnlySpan PrepareByteArray(int[] raster, int width, int height,int bitsPerPixel) { - int stride = GetStride(bmp); + int stride = 4 * ((width * bitsPerPixel + 31) / 32); + byte[] bits = new byte[stride * height]; // If no extra padding exists, copy entire rows at once. @@ -2679,50 +3096,6 @@ private static List CreateAnyBitmaps(IEnumerable imagePaths) return bitmaps; } - private static MemoryStream CreateMultiFrameImage(IEnumerable images, ImageFormat imageFormat = ImageFormat.Tiff) - { - FindMaxWidthAndHeight(images, out int maxWidth, out int maxHeight); - - Image result = null; - for (int i = 0; i < images.Count(); i++) - { - if (i == 0) - { - result = LoadAndResizeImageSharp(images.ElementAt(i).GetBytes(), maxWidth, maxHeight, i); - } - else - { - if (result == null) - { - result = LoadAndResizeImageSharp(images.ElementAt(i).GetBytes(), maxWidth, maxHeight, i); - } - else - { - Image image = - LoadAndResizeImageSharp(images.ElementAt(i).GetBytes(), maxWidth, maxHeight, i); - _ = result.Frames.AddFrame(image.Frames.RootFrame); - } - } - } - - MemoryStream resultStream = null; - if (result != null) - { - resultStream = new MemoryStream(); - if (imageFormat == ImageFormat.Gif) - { - result.SaveAsGif(resultStream); - } - else - { - result.SaveAsTiff(resultStream); - } - } - - result?.Dispose(); - - return resultStream; - } private static void FindMaxWidthAndHeight(IEnumerable images, out int maxWidth, out int maxHeight) { @@ -2736,57 +3109,11 @@ private static void FindMaxWidthAndHeight(IEnumerable images, out int maxHeight = images.Select(img => img.Height).Max(); } - private static Image CloneAndResizeImageSharp( - Image source, int maxWidth, int maxHeight) - { - using Image image = - source.CloneAs(); - // Keep Image dimension the same - return ResizeWithPadToPng(image, maxWidth, maxHeight); - } - - private static Image LoadAndResizeImageSharp(byte[] bytes, - int maxWidth, int maxHeight, int index) - { - try - { - using var result = - Image.Load(bytes); - // Keep Image dimension the same - return ResizeWithPadToPng(result, maxWidth, maxHeight); - } - catch (Exception e) - { - throw new NotSupportedException($"Image index {index} cannot " + - $"be loaded. File format doesn't supported.", e); - } - } - - private static Image ResizeWithPadToPng( - Image result, int maxWidth, int maxHeight) - { - result.Mutate(img => img.Resize(new ResizeOptions - { - Size = new Size(maxWidth, maxHeight), - Mode = ResizeMode.BoxPad, - PadColor = SixLabors.ImageSharp.Color.Transparent - })); - - using var memoryStream = new MemoryStream(); - result.Save(memoryStream, new PngEncoder - { - TransparentColorMode = PngTransparentColorMode.Preserve - }); - _ = memoryStream.Seek(0, SeekOrigin.Begin); - - return Image.Load(memoryStream); - } - private int GetStride(Image source = null) { if (source == null) { - return 4 * (((Image.Width * Image.PixelType.BitsPerPixel) + 31) / 32); + return 4 * (((Width * BitsPerPixel) + 31) / 32); } else { @@ -2796,10 +3123,18 @@ private int GetStride(Image source = null) private IntPtr GetFirstPixelData() { - byte[] pixelBytes = new byte[Image.Width * Image.Height * Unsafe.SizeOf()]; - Image clonedImage = Image.CloneAs(); - clonedImage.CopyPixelDataTo(pixelBytes); - ConvertRGBAtoBGRA(pixelBytes, clonedImage.Width, clonedImage.Height); + var image = GetFirstInternalImage(); + + if(image is not Image) + { + image = image.CloneAs(); + } + + Image rgbaImage = (Image)image; + byte[] pixelBytes = new byte[rgbaImage.Width * rgbaImage.Height * Unsafe.SizeOf()]; + + rgbaImage.CopyPixelDataTo(pixelBytes); + ConvertRGBAtoBGRA(pixelBytes, rgbaImage.Width, rgbaImage.Height); IntPtr result = Marshal.AllocHGlobal(pixelBytes.Length); Marshal.Copy(pixelBytes, 0, result, pixelBytes.Length); @@ -2825,7 +3160,7 @@ private static void ConvertRGBAtoBGRA(byte[] data, int width, int height, int sa private Color GetPixelColor(int x, int y) { - switch (Image) + switch (GetFirstInternalImage()) { case Image imageAsFormat: return imageAsFormat[x, y]; @@ -2851,7 +3186,7 @@ private Color GetPixelColor(int x, int y) //CloneAs() is expensive! //Can throw out of memory exception, when this fucntion get called too much - using (Image converted = Image.CloneAs()) + using (Image converted = GetFirstInternalImage().CloneAs()) { return converted[x, y]; } @@ -2860,8 +3195,11 @@ private Color GetPixelColor(int x, int y) private void SetPixelColor(int x, int y, Color color) { - switch (Image) + switch (GetFirstInternalImage()) { + case Image imageAsFormat: + imageAsFormat[x, y] = color; + break; case Image imageAsFormat: imageAsFormat[x, y] = color; break; @@ -2877,27 +3215,64 @@ private void SetPixelColor(int x, int y, Color color) case Image imageAsFormat: imageAsFormat[x, y] = color; break; + case Image imageAsFormat: + imageAsFormat[x, y] = color; + break; + case Image imageAsFormat: + imageAsFormat[x, y] = color; + break; default: - (Image as Image)[x, y] = color; + (GetFirstInternalImage() as Image)[x, y] = color; break; } + IsDirty = true; } private void LoadAndResizeImage(AnyBitmap original, int width, int height) { + //this prevent case when original is changed before Lazy is loaded + Binary = original.Binary; + + _lazyImage = new Lazy>(() => + { + + using var image = Image.Load(Binary); + image.Mutate(img => img.Resize(width, height)); + + //update Binary + using var memoryStream = new MemoryStream(); + image.Save(memoryStream, GetDefaultImageEncoder(image.Width, image.Height)); + Binary = memoryStream.ToArray(); + + return [image]; + }); + + ForceLoadLazyImage(); + } + + private IImageEncoder GetDefaultImageExportEncoder(ImageFormat format = ImageFormat.Default, int lossy = 100) + { + return format switch + { + ImageFormat.Jpeg => new JpegEncoder() + { + Quality = lossy, #if NET6_0_OR_GREATER - using var image = Image.Load(original.Binary); - IImageFormat format = image.Metadata.DecodedImageFormat; + ColorType = JpegEncodingColor.Rgb #else - using var image = Image.Load(original.Binary, out IImageFormat format); + ColorType = JpegColorType.Rgb #endif - image.Mutate(img => img.Resize(width, height)); - byte[] pixelBytes = new byte[image.Width * image.Height * Unsafe.SizeOf()]; - image.CopyPixelDataTo(pixelBytes); + }, + ImageFormat.Gif => new GifEncoder(), + ImageFormat.Png => new PngEncoder(), + ImageFormat.Webp => new WebpEncoder() { Quality = lossy }, + ImageFormat.Tiff => new TiffEncoder() + { + Compression = TiffCompression - Image = image.Clone(); - Binary = pixelBytes; - Format = format; + }, + _ => GetDefaultImageEncoder(Width, Height) + }; } private static ImageFormat GetImageFormat(string filename) @@ -2948,6 +3323,161 @@ object ICloneable.Clone() return this.Clone(); } - #endregion + /// + /// return BmpEncoder for small image and PngEncoder for large image + /// + /// + /// + /// + private static IImageEncoder GetDefaultImageEncoder(int imageWidth, int imageHeight) + { + return new BmpEncoder { BitsPerPixel = BmpBitsPerPixel.Pixel32, SupportTransparency = true }; + } + + private static void InternalSaveAsMultiPageTiff(IEnumerable images, Stream stream) + { + using (Tiff output = Tiff.ClientOpen("in-memory", "w", null, new NonClosingTiffStream(stream))) + { + foreach (var image in images) + { + int width = image.Width; + int height = image.Height; + int stride = width * 4; // RGBA => 4 bytes per pixel + + // Convert to byte[] in BGRA format as required by LibTiff + byte[] buffer = new byte[height * stride]; + + switch (image) + { + case Image imageAsFormat: + imageAsFormat.CopyPixelDataTo(buffer); + break; + case Image imageAsFormat: + imageAsFormat.CopyPixelDataTo(buffer); + break; + case Image imageAsFormat: + imageAsFormat.CopyPixelDataTo(buffer); + break; + case Image imageAsFormat: + imageAsFormat.CopyPixelDataTo(buffer); + break; + case Image imageAsFormat: + imageAsFormat.CopyPixelDataTo(buffer); + break; + default: + (image as Image).CopyPixelDataTo(buffer); + break; + } + + + //Note: TiffMetadata in current ImageSharp 3.1.8 is not good enough. but in the main branch of ImageSharp it looks good. + //TODO: revisit this TiffMetadata once release version of ImageSharp include new TiffMetadata implementation. + //TiffMetadata metadata = image.Metadata.GetTiffMetadata(); + + switch (image.Metadata.ResolutionUnits) + { + case SixLabors.ImageSharp.Metadata.PixelResolutionUnit.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: + 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: + 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: + output.SetField(TiffTag.XRESOLUTION, image.Metadata.HorizontalResolution * 100); + output.SetField(TiffTag.YRESOLUTION, image.Metadata.VerticalResolution * 100); + output.SetField(TiffTag.RESOLUTIONUNIT, ResUnit.CENTIMETER); + break; + } + + + output.SetField(TiffTag.IMAGEWIDTH, width); + output.SetField(TiffTag.IMAGELENGTH, height); + output.SetField(TiffTag.SAMPLESPERPIXEL, 4); + output.SetField(TiffTag.BITSPERSAMPLE, 8, 8, 8, 8); + output.SetField(TiffTag.ORIENTATION, Orientation.TOPLEFT); + output.SetField(TiffTag.ROWSPERSTRIP, height); + output.SetField(TiffTag.PLANARCONFIG, PlanarConfig.CONTIG); + output.SetField(TiffTag.PHOTOMETRIC, Photometric.RGB); + output.SetField(TiffTag.COMPRESSION, Compression.LZW); // optional + output.SetField(TiffTag.EXTRASAMPLES, 1, new short[] { (short)ExtraSample.ASSOCALPHA }); + + // Write each scanline + for (int row = 0; row < height; row++) + { + int offset = row * stride; + output.WriteScanline(buffer, offset, row, 0); + } + + output.WriteDirectory(); // Next page + } + } + stream.Position = 0; + } + private static void InternalSaveAsMultiPageGif(IEnumerable images, Stream stream) + { + // Find the maximum dimensions to create a logical screen that can fit all frames. + int maxWidth = images.Max(f => f.Width); + int maxHeight = images.Max(f => f.Height); + + using var gif = images.First().Clone(ctx => ctx.Resize(new ResizeOptions + { + Size = new Size(maxWidth, maxHeight), + Mode = ResizeMode.BoxPad, // Pad to fit the target dimensions + PadColor = Color.Transparent, // Use transparent padding + Position = AnchorPositionMode.Center // Center the image within the frame + })); + + + foreach (var sourceImage in images.Skip(1)) + { + // Clone the source image and apply the more efficient Resize operation. + // This resizes the image to fit, pads the rest with a transparent color, + // and centers it, all in one step. + using (var resizedFrame = sourceImage.Clone(ctx => ctx.Resize(new ResizeOptions + { + Size = new Size(maxWidth, maxHeight), + Mode = ResizeMode.BoxPad, // Pad to fit the target dimensions + PadColor = Color.Transparent, // Use transparent padding + Position = AnchorPositionMode.Center // Center the image within the frame + }))) + { + // Add the correctly-sized new frame to the master GIF's frame collection. + gif.Frames.AddFrame(resizedFrame.Frames.RootFrame); + } + } + // Save the final result to the provided stream. + gif.SaveAsGif(stream); + stream.Position = 0; + } + +#endregion + + /// + /// Check if image is loaded (decoded) + /// + /// true if images is loaded (decoded) into the memory + [Browsable(false)] + [Bindable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public bool IsImageLoaded() + { + if(_lazyImage == null) + { + return false; + } + else + { + return _lazyImage.IsValueCreated; + } + } } } \ No newline at end of file diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common/Color.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common/Color.cs index 3600abe..e3616d1 100644 --- a/IronSoftware.Drawing/IronSoftware.Drawing.Common/Color.cs +++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common/Color.cs @@ -1034,7 +1034,7 @@ public static implicit operator SixLabors.ImageSharp.PixelFormats.Rgba32(Color c /// will automatically be casted to public static implicit operator Color(SixLabors.ImageSharp.PixelFormats.Bgra32 color) { - return new Color(color.R, color.G, color.B, color.A); + return new Color(color.A, color.R, color.G, color.B); } /// @@ -1064,7 +1064,7 @@ 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) { - return SixLabors.ImageSharp.Color.FromRgb(color.R, color.G, color.B); + return SixLabors.ImageSharp.Color.FromRgba(color.R, color.G, color.B, color.A); } /// @@ -1094,7 +1094,7 @@ 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) { - return new Color(color.R, color.G, color.B); + return (Color)SixLabors.ImageSharp.Color.FromRgb((byte)(color.R >> 8), (byte)(color.G >> 8), (byte)(color.B >> 8)); } /// @@ -1104,7 +1104,9 @@ 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) { - return new SixLabors.ImageSharp.PixelFormats.Rgb48(color.R, color.G, color.B); + var result = new SixLabors.ImageSharp.PixelFormats.Rgb48(); + result.FromRgba64((SixLabors.ImageSharp.PixelFormats.Rgba64)color); + return result; } /// @@ -1144,7 +1146,7 @@ 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) { - return new SixLabors.ImageSharp.PixelFormats.Abgr32(color.R, color.G, color.B, color.A); + return SixLabors.ImageSharp.Color.FromRgba(color.R, color.G, color.B, color.A); } /// @@ -1164,7 +1166,7 @@ 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) { - return new SixLabors.ImageSharp.PixelFormats.Argb32(color.R, color.G, color.B, color.A); + return SixLabors.ImageSharp.Color.FromRgba(color.R, color.G, color.B, color.A); } /// diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common/NonClosingTiffStream.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common/NonClosingTiffStream.cs new file mode 100644 index 0000000..5632951 --- /dev/null +++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common/NonClosingTiffStream.cs @@ -0,0 +1,59 @@ +using BitMiracle.LibTiff.Classic; +using System; +using System.IO; + +namespace IronSoftware.Drawing +{ + internal class NonClosingTiffStream : TiffStream, IDisposable + { + private readonly Stream _stream; + private bool _disposed = false; + + public NonClosingTiffStream(Stream stream) + { + _stream = stream ?? throw new ArgumentNullException(nameof(stream)); + } + + public override int Read(object clientData, byte[] buffer, int offset, int count) + { + return _stream.Read(buffer, offset, count); + } + + public override void Write(object clientData, byte[] buffer, int offset, int count) + { + _stream.Write(buffer, offset, count); + } + + public override long Seek(object clientData, long offset, SeekOrigin origin) + { + return _stream.Seek(offset, origin); + } + + public override void Close(object clientData) + { + // Suppress automatic closing — manual control only + } + + public override long Size(object clientData) + { + return _stream.Length; + } + + /// + /// Manually closes the underlying stream when you are ready. + /// + public void CloseStream() + { + if (!_disposed) + { + _stream.Dispose(); + _disposed = true; + } + } + + public void Dispose() + { + CloseStream(); + } + } +} \ No newline at end of file