diff --git a/CI/azure-pipelines-build.yml b/CI/azure-pipelines-build.yml index c6364ae..59ae903 100644 --- a/CI/azure-pipelines-build.yml +++ b/CI/azure-pipelines-build.yml @@ -375,16 +375,3 @@ stages: publishSymbols: true symbolServerType: TeamServices detailedLog: true - - task: GitHubRelease@1 - inputs: - isPreRelease: true - gitHubConnection: 'ironsoftwarebuild' - repositoryName: 'iron-software/IronSoftware.System.Drawing' - action: 'create' - target: '$(Build.SourceVersion)' - tagSource: 'userSpecifiedTag' - tag: '$(NuGetVersion)' - title: 'IronSoftware.System.Drawing v$(NuGetVersion)' - releaseNotesSource: 'inline' - changeLogCompareToRelease: 'lastFullRelease' - changeLogType: 'commitBased' \ No newline at end of file diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/Data/IRON-274-39065.tif b/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/Data/IRON-274-39065.tif new file mode 100644 index 0000000..0f1bf2d Binary files /dev/null and b/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/Data/IRON-274-39065.tif differ diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/Data/animated_qr.gif b/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/Data/animated_qr.gif new file mode 100644 index 0000000..d0f8f81 Binary files /dev/null and b/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/Data/animated_qr.gif differ diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/Data/first-animated-qr.png b/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/Data/first-animated-qr.png new file mode 100644 index 0000000..3bc81d6 Binary files /dev/null and b/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/Data/first-animated-qr.png differ diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/Data/last-animated-qr.png b/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/Data/last-animated-qr.png new file mode 100644 index 0000000..8880f6c Binary files /dev/null and b/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/Data/last-animated-qr.png differ diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/Data/multiframe.tiff b/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/Data/multiframe.tiff new file mode 100644 index 0000000..bc1ec19 Binary files /dev/null and b/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/Data/multiframe.tiff differ diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/AnyBitmapFunctionality.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/AnyBitmapFunctionality.cs index 346d8b3..27db8b4 100644 --- a/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/AnyBitmapFunctionality.cs +++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/AnyBitmapFunctionality.cs @@ -1,6 +1,9 @@ using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; using System; +using System.Collections.Generic; using System.IO; +using System.Linq; using Xunit; using Xunit.Abstractions; @@ -268,6 +271,16 @@ public void Clone_AnyBitmap() clonedAnyBitmap.SaveAs("result.png"); AssertImageAreEqual("expected.png", "result.png", true); + + + using Image image = anyBitmap; + image.Mutate(img => img.Crop(new Rectangle(0, 0, 100, 100))); + AnyBitmap clonedWithRect = anyBitmap.Clone(new CropRectangle(0, 0, 100, 100)); + + image.SaveAsPng("expected.png"); + clonedWithRect.SaveAs("result.png"); + + AssertImageAreEqual("expected.png", "result.png", true); } [FactWithAutomaticDisplayName] @@ -361,6 +374,153 @@ public void CastSixLabors_from_AnyBitmap() AssertImageAreEqual("expected.bmp", "result.bmp", true); } + [FactWithAutomaticDisplayName] + public void Load_Tiff_Image() + { + AnyBitmap anyBitmap = AnyBitmap.FromFile(GetRelativeFilePath("IRON-274-39065.tif")); + Assert.Equal(2, anyBitmap.FrameCount); + + AnyBitmap multiPage = AnyBitmap.FromFile(GetRelativeFilePath("animated_qr.gif")); + Assert.Equal(4, multiPage.FrameCount); + Assert.Equal(4, multiPage.GetAllFrames.Count()); + multiPage.GetAllFrames.First().SaveAs("first.png"); + multiPage.GetAllFrames.Last().SaveAs("last.png"); + AssertImageAreEqual(GetRelativeFilePath("first-animated-qr.png"), "first.png"); + AssertImageAreEqual(GetRelativeFilePath("last-animated-qr.png"), "last.png"); + + byte[] bytes = File.ReadAllBytes(GetRelativeFilePath("IRON-274-39065.tif")); + anyBitmap = AnyBitmap.FromBytes(bytes); + Assert.Equal(2, anyBitmap.FrameCount); + + byte[] multiPageBytes = File.ReadAllBytes(GetRelativeFilePath("animated_qr.gif")); + multiPage = AnyBitmap.FromBytes(multiPageBytes); + Assert.Equal(4, multiPage.FrameCount); + Assert.Equal(4, multiPage.GetAllFrames.Count()); + multiPage.GetAllFrames.First().SaveAs("first.png"); + multiPage.GetAllFrames.Last().SaveAs("last.png"); + AssertImageAreEqual(GetRelativeFilePath("first-animated-qr.png"), "first.png"); + AssertImageAreEqual(GetRelativeFilePath("last-animated-qr.png"), "last.png"); + } + + [FactWithAutomaticDisplayName] + public void Try_UnLoad_Tiff_Image() + { + AnyBitmap anyBitmap = AnyBitmap.FromFile(GetRelativeFilePath("multiframe.tiff")); + Assert.Equal(2, anyBitmap.FrameCount); + } + + [FactWithAutomaticDisplayName] + public void Create_Multi_page_Tiff() + { + List bitmaps = new List() + { + AnyBitmap.FromFile(GetRelativeFilePath("first-animated-qr.png")), + AnyBitmap.FromFile(GetRelativeFilePath("last-animated-qr.png")) + }; + + AnyBitmap anyBitmap = AnyBitmap.CreateMultiFrameTiff(bitmaps); + Assert.Equal(2, anyBitmap.FrameCount); + Assert.Equal(2, anyBitmap.GetAllFrames.Count()); + anyBitmap.GetAllFrames.ElementAt(0).SaveAs("first.png"); + anyBitmap.GetAllFrames.ElementAt(1).SaveAs("last.png"); + AssertImageAreEqual(GetRelativeFilePath("first-animated-qr.png"), "first.png"); + AssertImageAreEqual(GetRelativeFilePath("last-animated-qr.png"), "last.png"); + } + + [FactWithAutomaticDisplayName] + public void Create_Multi_page_Tiff_Paths() + { + List imagePaths = new List() + { + GetRelativeFilePath("first-animated-qr.png"), + GetRelativeFilePath("last-animated-qr.png") + }; + + AnyBitmap anyBitmap = AnyBitmap.CreateMultiFrameTiff(imagePaths); + Assert.Equal(2, anyBitmap.FrameCount); + Assert.Equal(2, anyBitmap.GetAllFrames.Count()); + anyBitmap.GetAllFrames.ElementAt(0).SaveAs("first.png"); + anyBitmap.GetAllFrames.ElementAt(1).SaveAs("last.png"); + AssertImageAreEqual(GetRelativeFilePath("first-animated-qr.png"), "first.png"); + AssertImageAreEqual(GetRelativeFilePath("last-animated-qr.png"), "last.png"); + } + + [FactWithAutomaticDisplayName] + public void Create_Multi_page_Gif() + { + List bitmaps = new List() + { + AnyBitmap.FromFile(GetRelativeFilePath("first-animated-qr.png")), + AnyBitmap.FromFile(GetRelativeFilePath("mountainclimbers.jpg")) + }; + + AnyBitmap anyBitmap = AnyBitmap.CreateMultiFrameGif(bitmaps); + Assert.Equal(2, anyBitmap.FrameCount); + Assert.Equal(2, anyBitmap.GetAllFrames.Count()); + anyBitmap.GetAllFrames.ElementAt(0).SaveAs("first.png"); + Image first = Image.Load(GetRelativeFilePath("first-animated-qr.png")); + first.Mutate(img => img.Resize(new ResizeOptions + { + Size = new SixLabors.ImageSharp.Size(anyBitmap.GetAllFrames.ElementAt(0).Width, anyBitmap.GetAllFrames.ElementAt(0).Height), + Mode = SixLabors.ImageSharp.Processing.ResizeMode.BoxPad + })); + first.Save("first-expected.jpg"); + AssertImageAreEqual("first-expected.jpg", "first.png", true); + + anyBitmap.GetAllFrames.ElementAt(1).SaveAs("last.png"); + Image last = Image.Load(GetRelativeFilePath("mountainclimbers.jpg")); + last.Mutate(img => img.Resize(new ResizeOptions + { + Size = new SixLabors.ImageSharp.Size(anyBitmap.GetAllFrames.ElementAt(1).Width, anyBitmap.GetAllFrames.ElementAt(1).Height), + Mode = SixLabors.ImageSharp.Processing.ResizeMode.BoxPad + })); + last.Save("last-expected.jpg"); + AssertImageAreEqual("last-expected.jpg", "last.png", true); + } + + [FactWithAutomaticDisplayName] + public void Create_Multi_page_Gif_paths() + { + List imagePaths = new List() + { + GetRelativeFilePath("first-animated-qr.png"), + GetRelativeFilePath("mountainclimbers.jpg") + }; + + AnyBitmap anyBitmap = AnyBitmap.CreateMultiFrameGif(imagePaths); + Assert.Equal(2, anyBitmap.FrameCount); + Assert.Equal(2, anyBitmap.GetAllFrames.Count()); + anyBitmap.GetAllFrames.ElementAt(0).SaveAs("first.png"); + Image first = Image.Load(GetRelativeFilePath("first-animated-qr.png")); + first.Mutate(img => img.Resize(new ResizeOptions + { + Size = new SixLabors.ImageSharp.Size(anyBitmap.GetAllFrames.ElementAt(0).Width, anyBitmap.GetAllFrames.ElementAt(0).Height), + Mode = SixLabors.ImageSharp.Processing.ResizeMode.BoxPad + })); + first.Save("first-expected.jpg"); + AssertImageAreEqual("first-expected.jpg", "first.png", true); + + anyBitmap.GetAllFrames.ElementAt(1).SaveAs("last.png"); + Image last = Image.Load(GetRelativeFilePath("mountainclimbers.jpg")); + last.Mutate(img => img.Resize(new ResizeOptions + { + Size = new SixLabors.ImageSharp.Size(anyBitmap.GetAllFrames.ElementAt(1).Width, anyBitmap.GetAllFrames.ElementAt(1).Height), + Mode = SixLabors.ImageSharp.Processing.ResizeMode.BoxPad + })); + last.Save("last-expected.jpg"); + AssertImageAreEqual("last-expected.jpg", "last.png", true); + } + + [FactWithAutomaticDisplayName] + public void Should_Return_BitsPerPixel() + { + AnyBitmap bitmap = AnyBitmap.FromFile(GetRelativeFilePath("van-gogh-starry-night-vincent-van-gogh.jpg")); + Assert.Equal(24, bitmap.BitsPerPixel); + + bitmap = SixLabors.ImageSharp.Image.Load(GetRelativeFilePath("mountainclimbers.jpg")); + Assert.Equal(32, bitmap.BitsPerPixel); + } + #if !NET472 [FactWithAutomaticDisplayName] public void CastMaui_to_AnyBitmap() diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/ColorFunctionality.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/ColorFunctionality.cs index 12e1a0f..a4ee607 100644 --- a/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/ColorFunctionality.cs +++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/ColorFunctionality.cs @@ -487,6 +487,21 @@ public void Cast_ImageSharp_Rgba64_to_Color() Assert.Equal(45232, imgColor.B); } + [FactWithAutomaticDisplayName] + public void Should_Return_Argb() + { + System.Drawing.Color bmColor = System.Drawing.Color.Azure; + IronSoftware.Drawing.Color ironColor = IronSoftware.Drawing.Color.Azure; + IronSoftware.Drawing.Color fromImageSharp = SixLabors.ImageSharp.Color.Azure; + IronSoftware.Drawing.Color rgba32 = new SixLabors.ImageSharp.PixelFormats.Rgba32(bmColor.R, bmColor.G, bmColor.B, bmColor.A); + IronSoftware.Drawing.Color rgb24 = new SixLabors.ImageSharp.PixelFormats.Rgb24(bmColor.R, bmColor.G, bmColor.B); + + Assert.Equal(bmColor.ToArgb(), ironColor.ToArgb()); + Assert.Equal(bmColor.ToArgb(), fromImageSharp.ToArgb()); + Assert.Equal(bmColor.ToArgb(), rgba32.ToArgb()); + Assert.Equal(bmColor.ToArgb(), rgb24.ToArgb()); + } + #if !NET472 [FactWithAutomaticDisplayName] public void Cast_Maui_from_Color() diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/CropRectangleFunctionality.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/CropRectangleFunctionality.cs index c2c8c52..19e405d 100644 --- a/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/CropRectangleFunctionality.cs +++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common.Tests/UnitTests/CropRectangleFunctionality.cs @@ -246,6 +246,17 @@ public void CastSKRectI_from_CropRectangle() Assert.Equal(215, rect.Bottom); } + [FactWithAutomaticDisplayName] + public void ConvertMeasurement() + { + CropRectangle pxCropRect = new CropRectangle(15, 25, 150, 175); + CropRectangle mmCropRect = pxCropRect.ConvertTo(MeasurementUnits.Millimeters, 96); + Assert.Equal(3, mmCropRect.X); + Assert.Equal(6, mmCropRect.Y); + Assert.Equal(39, mmCropRect.Width); + Assert.Equal(46, mmCropRect.Height); + } + #if !NET472 [FactWithAutomaticDisplayName] diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common/AnyBitmap.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common/AnyBitmap.cs index d4b3d62..b8fc339 100644 --- a/IronSoftware.Drawing/IronSoftware.Drawing.Common/AnyBitmap.cs +++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common/AnyBitmap.cs @@ -1,16 +1,18 @@ -using SixLabors.ImageSharp; +using BitMiracle.LibTiff.Classic; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; -using System.Net.Http; using System.Reflection; namespace IronSoftware.Drawing { /// - /// A universally compatible Bitmap format for .NET 7 and .NET 6, .NET 5, .NET Core. Windows, NanoServer, IIS, macOS, Mobile, Xamarin, iOS, Android, Google Compute, Azure, AWS and Linux compatibility. + /// A universally compatible Bitmap format for .NET 7, .NET 6, .NET 5, and .NET Core. As well as compatiblity with Windows, NanoServer, IIS, macOS, Mobile, Xamarin, iOS, Android, Google Compute, Azure, AWS, and Linux. /// Works nicely with popular Image and Bitmap formats such as System.Drawing.Bitmap, SkiaSharp, SixLabors.ImageSharp, Microsoft.Maui.Graphics. /// Implicit casting means that using this class to input and output Bitmap and image types from public API's gives full compatibility to all image type fully supported by Microsoft. /// Unlike System.Drawing.Bitmap this bitmap object is self-memory-managing and does not need to be explicitly 'used' or 'disposed'. @@ -99,6 +101,19 @@ public AnyBitmap Clone() return new AnyBitmap(this.Binary); } + /// + /// Creates an exact duplicate of the cropped area. + /// + /// Defines the portion of this to copy. + /// + public AnyBitmap Clone(CropRectangle Rectangle) + { + using SixLabors.ImageSharp.Image image = Image.Clone(img => img.Crop(Rectangle)); + using var memoryStream = new System.IO.MemoryStream(); + image.Save(memoryStream, new SixLabors.ImageSharp.Formats.Bmp.BmpEncoder()); + return new AnyBitmap(memoryStream.ToArray()); + } + /// /// Exports the Bitmap as bytes encoded in the of your choice. /// Add SkiaSharp, System.Drawing.Common, or SixLabors.ImageSharp to your project to enable this feature. @@ -450,6 +465,131 @@ public static AnyBitmap FromUri(Uri Uri) } } + /// + /// Gets colors depth, in number of bits per pixel. + /// + public int BitsPerPixel + { + get + { + return Image.PixelType.BitsPerPixel; + } + } + + /// + /// 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 return 1. + /// + /// + public int FrameCount + { + get + { + return Image.Frames.Count; + } + } + + /// + /// Returns all of the cloned 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. + /// + /// + /// + public IEnumerable GetAllFrames + { + get + { + if (FrameCount > 1) + { + List images = new List(); + + for (int currFrameIndex = 0; currFrameIndex < FrameCount; currFrameIndex++) + { + images.Add(Image.Frames.CloneFrame(currFrameIndex)); + } + return images; + } + else + { + return new List() { this.Clone() }; + } + } + } + + /// + /// Creates a multi-frame TIFF image from multiple AnyBitmaps. + /// All images should have the same dimension. + /// If not dimension will be scaling to the largest width and height. + /// The image dimension still the same with original dimension with black background. + /// + /// Array of fully qualified file path to merge into Tiff image. + /// + public static AnyBitmap CreateMultiFrameTiff(IEnumerable imagePaths) + { + MemoryStream stream = CreateMultiFrameImage(CreateAnyBitmaps(imagePaths)); + + if (stream == null) + throw new NotSupportedException("Image could not be loaded. File format is not supported."); + + stream.Seek(0, SeekOrigin.Begin); + return AnyBitmap.FromStream(stream); + } + + /// + /// Creates a multi-frame TIFF image from multiple AnyBitmaps. + /// All images should have the same dimension. + /// If not dimension will be scaling to the largest width and height. + /// The image dimension still the same with original dimension with black background. + /// + /// Array of to merge into Tiff image. + /// + public static AnyBitmap CreateMultiFrameTiff(IEnumerable images) + { + MemoryStream stream = CreateMultiFrameImage(images); + + if (stream == null) + throw new NotSupportedException("Image could not be loaded. File format is not supported."); + + stream.Seek(0, SeekOrigin.Begin); + return AnyBitmap.FromStream(stream); + } + + /// + /// Creates a multi-frame GIF image from multiple AnyBitmaps. + /// All images should have the same dimension. + /// If not dimension will be scaling to the largest width and height. + /// The image dimension still the same with original dimension with background transparent. + /// + /// Array of fully qualified file path to merge into Gif image. + /// + public static AnyBitmap CreateMultiFrameGif(IEnumerable imagePaths) + { + MemoryStream stream = CreateMultiFrameImage(CreateAnyBitmaps(imagePaths), ImageFormat.Gif); + + if (stream == null) + throw new NotSupportedException("Image could not be loaded. File format is not supported."); + + stream.Seek(0, SeekOrigin.Begin); + return AnyBitmap.FromStream(stream); + } + + /// + /// Creates a multi-frame GIF image from multiple AnyBitmaps. + /// All images should have the same dimension. + /// If not dimension will be scaling to the largest width and height. + /// The image dimension still the same with original dimension with background transparent. + /// + /// Array of to merge into Gif image. + /// + public static AnyBitmap CreateMultiFrameGif(IEnumerable images) + { + MemoryStream stream = CreateMultiFrameImage(images, ImageFormat.Gif); + + if (stream == null) + throw new NotSupportedException("Image could not be loaded. File format is not supported."); + + stream.Seek(0, SeekOrigin.Begin); + return AnyBitmap.FromStream(stream); + } + /// /// 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. @@ -949,6 +1089,17 @@ private void LoadImage(byte[] Bytes) { throw new DllNotFoundException("Please install SixLabors.ImageSharp from NuGet.", e); } + catch (NotSupportedException e) + { + try + { + OpenTiffToImageSharp(Bytes); + } + catch + { + throw new NotSupportedException("Image could not be loaded. File format is not supported.", e); + } + } catch (Exception e) { throw new Exception("Error while loading image bytes.", e); @@ -967,12 +1118,33 @@ private void LoadImage(string File) { throw new DllNotFoundException("Please install SixLabors.ImageSharp from NuGet.", e); } + catch (NotSupportedException e) + { + try + { + OpenTiffToImageSharp(System.IO.File.ReadAllBytes(File)); + } + catch + { + throw new NotSupportedException("Image could not be loaded. File format is not supported.", e); + } + } catch (Exception e) { throw new Exception("Error while loading image file.", e); } } + private void SetBinaryFromImageSharp() + { + using (var memoryStream = new MemoryStream()) + { + Image.Save(memoryStream, new SixLabors.ImageSharp.Formats.Tiff.TiffEncoder()); + memoryStream.Seek(0, SeekOrigin.Begin); + Binary = memoryStream.ToArray(); + } + } + private void LoadImage(Stream stream) { byte[] buffer = new byte[16 * 1024]; @@ -1031,6 +1203,34 @@ private static SkiaSharp.SKBitmap DecodeSVG(string strInput) } } + private SixLabors.ImageSharp.Formats.IImageEncoder FindEncoder(string File) + { + if (File.ToLower().EndsWith(".jpg") || File.ToLower().EndsWith(".jpeg")) + { + return new SixLabors.ImageSharp.Formats.Jpeg.JpegEncoder(); + } + else if (File.ToLower().EndsWith(".gif")) + { + return new SixLabors.ImageSharp.Formats.Gif.GifEncoder(); + } + else if (File.ToLower().EndsWith(".png")) + { + return new SixLabors.ImageSharp.Formats.Png.PngEncoder(); + } + else if (File.ToLower().EndsWith(".webp")) + { + return new SixLabors.ImageSharp.Formats.Webp.WebpEncoder(); + } + else if (File.ToLower().EndsWith(".tif") || File.ToLower().EndsWith(".tiff")) + { + return new SixLabors.ImageSharp.Formats.Tiff.TiffEncoder(); + } + else + { + return new SixLabors.ImageSharp.Formats.Bmp.BmpEncoder(); + } + } + private List TryExportStream(System.IO.Stream Stream, ImageFormat Format = ImageFormat.Default, int Lossy = 100) { List exceptions = new List(); @@ -1260,6 +1460,202 @@ private static SkiaSharp.SKBitmap OpenTiffToSKBitmap(AnyBitmap anyBitmap) } } + private void OpenTiffToImageSharp(byte[] bytes) + { + try + { + List images = new List(); + + // create a memory stream out of them + MemoryStream tiffStream = new MemoryStream(bytes); + + // open a TIFF stored in the stream + using (var tif = BitMiracle.LibTiff.Classic.Tiff.ClientOpen("in-memory", "r", tiffStream, new BitMiracle.LibTiff.Classic.TiffStream())) + { + var num = tif.NumberOfDirectories(); + for (short i = 0; i < num; i++) + { + tif.SetDirectory(i); + + // Find the width and height of the image + FieldValue[] value = tif.GetField(TiffTag.IMAGEWIDTH); + int width = value[0].ToInt(); + + value = tif.GetField(TiffTag.IMAGELENGTH); + int height = value[0].ToInt(); + + // Read the image into the memory buffer + int[] raster = new int[height * width]; + if (!tif.ReadRGBAImageOriented(width, height, raster, Orientation.TOPLEFT)) + { + throw new Exception("Could not read image"); + } + + using Image bmp = new Image(width, height); + SixLabors.ImageSharp.Rectangle rect = new SixLabors.ImageSharp.Rectangle(0, 0, bmp.Width, bmp.Height); + + int stride = 4 * ((bmp.Width * bmp.PixelType.BitsPerPixel + 31) / 32); + + bmp.ProcessPixelRows(accessor => + { + for (int y = 0; y < accessor.Height; y++) + { + int rasterOffset = y * bmp.Width; + int bitsOffset = (bmp.Height - y - 1) * stride; + Span pixelRow = accessor.GetRowSpan(y); + + for (int x = 0; x < pixelRow.Length; x++) + { + ref Rgba32 pixel = ref pixelRow[x]; + + int rgba = raster[rasterOffset++]; + pixel.R = (byte)((rgba >> 16) & 0xff); + pixel.G = (byte)((rgba >> 8) & 0xff); + pixel.B = (byte)(rgba & 0xff); + pixel.A = (byte)((rgba >> 24) & 0xff); + } + } + }); + + images.Add(bmp.Clone()); + } + } + + if (Image != null) + Image.Dispose(); + + FindMaxWidthAndHeight(images, out int maxWidth, out int maxHeight); + + for (int i = 0; i < images.Count; i++) + { + if (i == 0) + { + Image = CloneAndResizeImageSharp(images[i], maxWidth, maxHeight); + } + else + { + Image image = CloneAndResizeImageSharp(images[i], maxWidth, maxHeight); + Image.Frames.AddFrame(image.Frames.RootFrame); + } + } + SetBinaryFromImageSharp(); + } + catch (DllNotFoundException e) + { + throw new DllNotFoundException("Please install BitMiracle.LibTiff.NET from NuGet.", e); + } + catch (Exception e) + { + throw new Exception("Error while reading TIFF image format.", e); + } + } + + private static List CreateAnyBitmaps(IEnumerable imagePaths) + { + List bitmaps = new List(); + foreach (string imagePath in imagePaths) + { + bitmaps.Add(AnyBitmap.FromFile(imagePath)); + } + 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); + } + } + + return resultStream; + } + + private static void FindMaxWidthAndHeight(IEnumerable images, out int maxWidth, out int maxHeight) + { + maxWidth = images.Select(img => img.Width).Max(); + maxHeight = images.Select(img => img.Height).Max(); + } + + private static void FindMaxWidthAndHeight(IEnumerable images, out int maxWidth, out int maxHeight) + { + maxWidth = images.Select(img => img.Width).Max(); + maxHeight = images.Select(img => img.Height).Max(); + } + + private Image CloneAndResizeImageSharp(Image source, int maxWidth, int maxHeight) + { + 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 Image result = SixLabors.ImageSharp.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 = SixLabors.ImageSharp.Processing.ResizeMode.BoxPad, + PadColor = SixLabors.ImageSharp.Color.Transparent + })); + + using (var memoryStream = new MemoryStream()) + { + result.Save(memoryStream, new SixLabors.ImageSharp.Formats.Png.PngEncoder + { + TransparentColorMode = SixLabors.ImageSharp.Formats.Png.PngTransparentColorMode.Preserve + }); + memoryStream.Seek(0, SeekOrigin.Begin); + + return SixLabors.ImageSharp.Image.Load(memoryStream); + } + } + #endregion } } diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common/Color.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common/Color.cs index 2f63c6a..c546257 100644 --- a/IronSoftware.Drawing/IronSoftware.Drawing.Common/Color.cs +++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common/Color.cs @@ -4,33 +4,42 @@ namespace IronSoftware.Drawing { + // + /// A universally compatible Color for .NET 7, .NET 6, .NET 5, and .NET Core. As well as compatiblity with Windows, NanoServer, IIS, macOS, Mobile, Xamarin, iOS, Android, Google Compute, Azure, AWS, and Linux. + /// Works nicely with popular Image Color such as , , , . + /// Implicit casting means that using this class to input and output Color from public APIs gives full compatibility to all Color-types fully supported by Microsoft. + /// public partial class Color { /// - /// Gets the alpha component value of this System.Drawing.Color structure. + /// Gets the alpha component value of this IronSoftware.Drawing.Color structure. /// /// The alpha component value of this IronSoftware.Drawing.Color. public byte A { get; internal set; } /// - /// Gets the green component value of this System.Drawing.Color structure. + /// Gets the green component value of this IronSoftware.Drawing.Color structure. /// /// The green component value of this IronSoftware.Drawing.Color. public byte G { get; internal set; } /// - /// Gets the blue component value of this System.Drawing.Color structure. + /// Gets the blue component value of this IronSoftware.Drawing.Color structure. /// /// The blue component value of this IronSoftware.Drawing.Color. public byte B { get; internal set; } /// - /// Gets the red component value of this System.Drawing.Color structure. + /// Gets the red component value of this IronSoftware.Drawing.Color structure. /// /// The red component value of this IronSoftware.Drawing.Color. public byte R { get; internal set; } + /// + /// Construct a new Color. + /// + /// The hexadecimal representation of the combined color components arranged in rgb, argb, rrggbb, or aarrggbb format to match web syntax. public Color(string colorcode) { string trimmedColorcode = colorcode.TrimStart('#'); @@ -62,6 +71,13 @@ public Color(string colorcode) } } + /// + /// Construct a new Color. + /// + /// The alpha component. Valid values are 0 through 255. + /// The red component. Valid values are 0 through 255. + /// The green component. Valid values are 0 through 255. + /// The blue component. Valid values are 0 through 255. public Color(int alpha, int red, int green, int blue) { this.A = (byte)alpha; @@ -70,6 +86,12 @@ public Color(int alpha, int red, int green, int blue) this.B = (byte)blue; } + /// + /// Construct a new Color. + /// + /// The red component. Valid values are 0 through 255. + /// The green component. Valid values are 0 through 255. + /// The blue component. Valid values are 0 through 255. public Color(int red, int green, int blue) { this.A = 255; @@ -795,9 +817,9 @@ public Color(int red, int green, int blue) /// this method allows a 32-bit value to be passed for each color component, the /// value of each component is limited to 8 bits. /// - /// The red component value for the new System.Drawing.Color. Valid values are 0 through 255. - /// The green component value for the new System.Drawing.Color. Valid values are 0 through 255. - /// The blue component value for the new System.Drawing.Color. Valid values are 0 through 255. + /// The red component value for the new IronSoftware.Drawing.Color. Valid values are 0 through 255. + /// The green component value for the new IronSoftware.Drawing.Color. Valid values are 0 through 255. + /// The blue component value for the new IronSoftware.Drawing.Color. Valid values are 0 through 255. /// public static Color FromArgb(int red, int green, int blue) { @@ -809,10 +831,10 @@ public static Color FromArgb(int red, int green, int blue) /// (alpha, red, green, and blue). Although this method allows a 32-bit value to be passed for each color component, /// the value of each component is limited to 8 bits. /// - /// The alpha value for the new System.Drawing.Color. Valid values are 0 through 255. - /// The red component value for the new System.Drawing.Color. Valid values are 0 through 255. - /// The green component value for the new System.Drawing.Color. Valid values are 0 through 255. - /// The blue component value for the new System.Drawing.Color. Valid values are 0 through 255. + /// The alpha value for the new IronSoftware.Drawing.Color. Valid values are 0 through 255. + /// The red component value for the new IronSoftware.Drawing.Color. Valid values are 0 through 255. + /// The green component value for the new IronSoftware.Drawing.Color. Valid values are 0 through 255. + /// The blue component value for the new IronSoftware.Drawing.Color. Valid values are 0 through 255. /// public static Color FromArgb(int alpha, int red, int green, int blue) { @@ -848,6 +870,15 @@ public double GetLuminance() return Math.Round(Percentage(255, CalculateLuminance()), MidpointRounding.AwayFromZero); } + /// + /// Gets the 32-bit ARGB value of this Color structure. + /// + /// The 32-bit ARGB value of this Color. + public int ToArgb() + { + return (this.A << 24) | (this.R << 16) | (this.G << 8) | this.B; + } + /// /// Implicitly casts System.Drawing.Color objects to . /// When your .NET Class methods use as parameters or return types, you now automatically support System.Drawing.Color as well. diff --git a/IronSoftware.Drawing/IronSoftware.Drawing.Common/CropRectangle.cs b/IronSoftware.Drawing/IronSoftware.Drawing.Common/CropRectangle.cs index a342337..1431725 100644 --- a/IronSoftware.Drawing/IronSoftware.Drawing.Common/CropRectangle.cs +++ b/IronSoftware.Drawing/IronSoftware.Drawing.Common/CropRectangle.cs @@ -1,15 +1,36 @@ +using System; + namespace IronSoftware.Drawing { + // + /// A universally compatible Rectangle for .NET 7, .NET 6, .NET 5, and .NET Core. As well as compatiblity with Windows, NanoServer, IIS, macOS, Mobile, Xamarin, iOS, Android, Google Compute, Azure, AWS, and Linux. + /// Works nicely with popular Image Rectangle such as System.Drawing.Rectangle, SkiaSharp.SKRect, SixLabors.ImageSharp.Rectangle, Microsoft.Maui.Graphics.Rect. + /// Implicit casting means that using this class to input and output Rectangle from public API's gives full compatibility to all Rectangle type fully supported by Microsoft. + /// public partial class CropRectangle { + /// + /// Construct a new CropRectangle. + /// + /// public CropRectangle() { } - public CropRectangle(int x, int y, int width, int height) + /// + /// Construct a new CropRectangle. + /// + /// The x-coordinate of the upper-left corner of this Rectangle + /// The y-coordinate of the upper-left corner of this Rectangle + /// The width of this Rectangle + /// The height of this Rectangle + /// The measurement unit of this Rectangle + /// + public CropRectangle(int x, int y, int width, int height, MeasurementUnits units = MeasurementUnits.Pixels) { this.X = x; this.Y = y; this.Width = width; this.Height = height; + this.Units = units; } /// @@ -28,6 +49,48 @@ public CropRectangle(int x, int y, int width, int height) /// The height of this Rectangle. The default is 0. /// public int Height { get; set; } + /// + /// The measurement unit of this Rectangle. The default is Pixels + /// + public MeasurementUnits Units + { + get; + set; + } = MeasurementUnits.Pixels; + + /// + /// Convert this crop rectangle to the specified units of measurement using the specified DPI + /// + /// Unit of measurement + /// DPI (Dots per inch) for conversion + /// A new crop rectangle which uses the desired units of measurement + /// Conversion not implemented + public CropRectangle ConvertTo(MeasurementUnits units, int dpi = 96) + { + // no conversion + if (units == this.Units) + return this; + // convert mm to pixels + if (units == MeasurementUnits.Pixels) + { + int x = (int)(((double)this.X / 25.4) * (double)dpi); + int y = (int)(((double)this.Y / 25.4) * (double)dpi); + int width = (int)(((double)this.Width / 25.4) * (double)dpi); + int height = (int)(((double)this.Height / 25.4) * (double)dpi); + return new CropRectangle(x, y, width, height, MeasurementUnits.Pixels); + } + // convert pixels to mm + if (units == MeasurementUnits.Millimeters) + { + int x = (int)((this.X / (double)dpi) * 25.4); + int y = (int)((this.Y / (double)dpi) * 25.4); + int width = (int)((this.Width / (double)dpi) * 25.4); + int height = (int)((this.Height / (double)dpi) * 25.4); + return new CropRectangle(x, y, width, height, MeasurementUnits.Millimeters); + + } + throw new NotImplementedException($"CropRectangle conversion from {this.Units} to {units} is not implemented"); + } /// /// Implicitly casts System.Drawing.Rectangle objects to . @@ -180,4 +243,19 @@ private static CropRectangle CreateCropRectangle(int left, int top, int right, i #endregion } + + /// + /// A defined unit of measurement + /// + public enum MeasurementUnits : int + { + /// + /// Pixels + /// + Pixels = 0, + /// + /// Millimeters + /// + Millimeters = 1 + } } diff --git a/NuGet/IronSoftware.Drawing.nuspec b/NuGet/IronSoftware.Drawing.nuspec index 6b56bfa..9d8d463 100644 --- a/NuGet/IronSoftware.Drawing.nuspec +++ b/NuGet/IronSoftware.Drawing.nuspec @@ -50,9 +50,11 @@ All new classes developed to support conversion between: (System.Drawing, SixLab + + diff --git a/README.md b/README.md index df7ca77..5d36185 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,27 @@ bimtap.ExportStream(resultExport, AnyBitmap.ImageFormat.Jpeg, 100); System.Drawing.Bitmap image = new System.Drawing.Bitmap("FILE_PATH"); IronSoftware.Drawing.AnyBitmap anyBitmap = image; anyBitmap.SaveAs("result-from-casting.png"); + + +// Creates a Multi-page Tiff-style AnyBitmap from an Image array +List bitmaps = new List() +{ + AnyBitmap.FromFile("FILE_PATH_1"), + AnyBitmap.FromFile("FILE_PATH_2") +}; +AnyBitmap anyBitmap = AnyBitmap.CreateMultiFrameTiff(bitmaps); + +// Creates a Multi-page Tiff-style AnyBitmap from a fully qualified file path array +List imagePaths = new List() +{ + "FILE_PATH_1", + "FILE_PATH_2" +}; +AnyBitmap anyBitmap = AnyBitmap.CreateMultiFrameTiff(imagePaths); + +// Manipulate image frames +int frameCount = anyBitmap.FrameCount; +List frames = anyBitmap.GetAllFrames(); ``` ### `Color` Code Example ```csharp @@ -88,7 +109,10 @@ ironColor.G; ironColor.B; ​ // Luminance is a value from 0 (black) to 100 (white) where 50 is the perceptual "middle grey" -IronDrawingColor.GetLuminance(); +ironColor.GetLuminance(); + +// Gets the 32-bit ARGB value of this Color structure. +ironColor.ToArgb() ``` ### `CropRectangle` Code Example ```csharp @@ -96,6 +120,14 @@ using IronSoftware.Drawing; ​ // Create a new CropRectangle object CropRectangle cropRectangle = new CropRectangle(5, 5, 50, 50); + +// Create a new CropRectangle object with MeasurementUnits +CropRectangle mmRectangle = new CropRectangle(5, 5, 50, 50, MeasurementUnits.Millimeters); + +// Convert between MeasurementUnits +CropRectangle pxRectangle = mmRectangle.ConvertTo(MeasurementUnits.Millimeters); +// Or specify DPI +CropRectangle pxRectangleWithDPI = mmRectangle.ConvertTo(MeasurementUnits.Millimeters, 200); ​ // Casting between System.Drawing.Rectangle and IronSoftware.Drawing.CropRectangle System.Drawing.Rectangle rectangle = new System.Drawing.Rectangle(10, 10, 150, 150);