Skip to content

Commit

Permalink
Merge pull request #110 from iron-software/releases/2024.7
Browse files Browse the repository at this point in the history
Releases/2024.7 [master]
  • Loading branch information
first-ironsoftware authored Jun 20, 2024
2 parents 984c891 + eb555ca commit e534614
Show file tree
Hide file tree
Showing 8 changed files with 153 additions and 23 deletions.
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<PackageReference Include="FluentAssertions" Version="6.10.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="Microsoft.Maui.Graphics" Version="7.0.92" />
<PackageReference Include="runtime.osx.10.10-x64.CoreCompat.System.Drawing" Version="6.0.5.128" />
<PackageReference Include="SkiaSharp" Version="2.88.7" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.88.7" />
<PackageReference Include="System.Drawing.Common" Version="6.0.0" />
Expand All @@ -29,6 +30,7 @@

<ItemGroup>
<ProjectReference Include="..\IronSoftware.Drawing.Common\IronSoftware.Drawing.Common.csproj" />
<RuntimeHostConfigurationOption Include="System.Drawing.EnableUnixSupport" Value="true" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,5 +69,18 @@ protected static bool IsUnix()
{
return RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
}

/// <summary>
/// Get current processor architecture/bittiness string
/// </summary>
/// <returns>String representing architecture of the current process</returns>
public static string GetArchitecture()
{
return RuntimeInformation.ProcessArchitecture == Architecture.Arm64
? "arm64"
: Environment.Is64BitProcess
? "x64"
: "x86";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
using SixLabors.ImageSharp.Processing;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using Xunit;
using Xunit.Abstractions;
using Image = SixLabors.ImageSharp.Image;

namespace IronSoftware.Drawing.Common.Tests.UnitTests
{
Expand Down Expand Up @@ -407,6 +409,19 @@ public void CastSixLabors_from_AnyBitmap()
AssertImageAreEqual("expected.bmp", "result.bmp", true);
}

[FactWithAutomaticDisplayName]
public void CastBitmap_to_AnyBitmap_using_FromBitmap()
{
string imagePath = GetRelativeFilePath("van-gogh-starry-night-vincent-van-gogh.jpg");
System.Drawing.Bitmap bitmap = new System.Drawing.Bitmap(imagePath);
AnyBitmap anyBitmap = AnyBitmap.FromBitmap(bitmap);

bitmap.Save("expected.png");
anyBitmap.SaveAs("result.png");

AssertImageAreEqual("expected.png", "result.png", true);
}

[FactWithAutomaticDisplayName]
public void Load_Tiff_Image()
{
Expand Down Expand Up @@ -463,11 +478,13 @@ public void Create_Multi_page_Tiff()
[FactWithAutomaticDisplayName]
public void Create_Multi_page_Tiff_Paths()
{
string outputImagePath = "create-tiff-output.tif";
var imagePaths = new List<string>()
{
GetRelativeFilePath("first-animated-qr.png"),
GetRelativeFilePath("last-animated-qr.png")
};
long maxInputFileSize = imagePaths.Select(path => new FileInfo(path).Length).Max();

var anyBitmap = AnyBitmap.CreateMultiFrameTiff(imagePaths);
Assert.Equal(2, anyBitmap.FrameCount);
Expand All @@ -476,6 +493,13 @@ public void Create_Multi_page_Tiff_Paths()
anyBitmap.GetAllFrames.ElementAt(1).SaveAs("last.png");
AssertImageAreEqual(GetRelativeFilePath("first-animated-qr.png"), "first.png");
AssertImageAreEqual(GetRelativeFilePath("last-animated-qr.png"), "last.png");

anyBitmap.SaveAs(outputImagePath);

long outputFileSize = new FileInfo(outputImagePath).Length;
outputFileSize.Should().BeLessThanOrEqualTo(maxInputFileSize, $"Output file size ({outputFileSize}) exceeds the maximum input file size ({maxInputFileSize}).");

File.Delete(outputImagePath);
}

[FactWithAutomaticDisplayName]
Expand Down Expand Up @@ -866,7 +890,7 @@ public void LoadImage_TiffImage_ShouldLoadWithoutThumbnail()
bitmap.FrameCount.Should().Be(1);
}


#if !NET7_0
[FactWithAutomaticDisplayName]
public void CastAnyBitmap_from_SixLabors()
{
Expand All @@ -881,6 +905,34 @@ public void CastAnyBitmap_from_SixLabors()

AssertLargeImageAreEqual("expected.bmp", "result.bmp", true);
}
#endif


[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);
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System;
using IronSoftware.Drawing.Common.Tests;
using Xunit;

public sealed class IgnoreOnAzureDevopsX86FactAttribute : FactWithAutomaticDisplayNameAttribute
{
/// <summary>
/// This class using for Skip test on Azure Devops and x86 architect
/// <para>Regarding to OcrWizardTests.RunningWizardShouldFindBetterResult always failed on Devops x86 tested</para>
/// </summary>
public IgnoreOnAzureDevopsX86FactAttribute()
{
if (!(IsRunningOnAzureDevOps() && IsRunningOnX86()))
{
return;
}

Skip = "Ignored on Azure DevOps";
}

/// <summary>Determine if runtime is Azure DevOps.</summary>
/// <returns>True if being executed in Azure DevOps, false otherwise.</returns>
public static bool IsRunningOnAzureDevOps()
{
return Environment.GetEnvironmentVariable("SYSTEM_DEFINITIONID") != null;
}

// <summary>Determine if runtime is x86 architect.</summary>
/// <returns>True if being executed in x86 architect, false otherwise.</returns>
public static bool IsRunningOnX86()
{
return TestsBase.GetArchitecture() == "x86";
}
}
67 changes: 47 additions & 20 deletions IronSoftware.Drawing/IronSoftware.Drawing.Common/AnyBitmap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Tiff;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
Expand All @@ -21,7 +22,6 @@
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Threading.Tasks;

namespace IronSoftware.Drawing
Expand Down Expand Up @@ -50,6 +50,7 @@ public partial class AnyBitmap : IDisposable
private Image Image { get; set; }
private byte[] Binary { get; set; }
private IImageFormat Format { get; set; }
private TiffCompression TiffCompression { get; set; } = TiffCompression.Lzw;

/// <summary>
/// Width of the image.
Expand Down Expand Up @@ -292,7 +293,10 @@ public void ExportStream(
ImageFormat.Gif => new GifEncoder(),
ImageFormat.Png => new PngEncoder(),
ImageFormat.Webp => new WebpEncoder() { Quality = lossy },
ImageFormat.Tiff => new TiffEncoder(),
ImageFormat.Tiff => new TiffEncoder()
{
Compression = TiffCompression
},
_ => new BmpEncoder()
{
BitsPerPixel = BmpBitsPerPixel.Pixel32,
Expand Down Expand Up @@ -410,10 +414,10 @@ public static AnyBitmap FromBitmap<T>(T otherBitmapFormat)
{
try
{
var result = (AnyBitmap)Convert.ChangeType(
var bitmap = (System.Drawing.Bitmap)Convert.ChangeType(
otherBitmapFormat,
typeof(AnyBitmap));
return result;
typeof(System.Drawing.Bitmap));
return (AnyBitmap)bitmap;
}
catch (Exception e)
{
Expand Down Expand Up @@ -1321,7 +1325,7 @@ public static implicit operator Image(AnyBitmap bitmap)
{
try
{
return Image.Load<Rgba32>(bitmap.Binary);
return Image.Load(bitmap.Binary);
}
catch (DllNotFoundException e)
{
Expand Down Expand Up @@ -2203,23 +2207,25 @@ private void OpenTiffToImageSharp(ReadOnlySpan<byte> bytes)
using MemoryStream tiffStream = new(bytes.ToArray());

// open a TIFF stored in the stream
using (Tiff tif = Tiff.ClientOpen("in-memory", "r", tiffStream, new TiffStream()))
using (Tiff tiff = Tiff.ClientOpen("in-memory", "r", tiffStream, new TiffStream()))
{
short num = tif.NumberOfDirectories();
SetTiffCompression(tiff);

short num = tiff.NumberOfDirectories();
for (short i = 0; i < num; i++)
{
_ = tif.SetDirectory(i);
_ = tiff.SetDirectory(i);

if (IsThumbnail(tif))
if (IsThumbnail(tiff))
{
continue;
}

var (width, height) = SetWidthHeight(tif, i, ref imageWidth, ref imageHeight);
var (width, height) = SetWidthHeight(tiff, i, ref imageWidth, ref imageHeight);

// Read the image into the memory buffer
int[] raster = new int[height * width];
if (!tif.ReadRGBAImage(width, height, raster))
if (!tiff.ReadRGBAImage(width, height, raster))
{
throw new NotSupportedException("Could not read image");
}
Expand All @@ -2231,7 +2237,7 @@ private void OpenTiffToImageSharp(ReadOnlySpan<byte> bytes)
images.Add(Image.LoadPixelData<Rgba32>(bits, bmp.Width, bmp.Height));
}
}

// find max
FindMaxWidthAndHeight(images, out int maxWidth, out int maxHeight);

Expand Down Expand Up @@ -2259,7 +2265,7 @@ private void OpenTiffToImageSharp(ReadOnlySpan<byte> bytes)

// dispose images past the first
images[i].Dispose();
}
}

// get raw binary
using var memoryStream = new MemoryStream();
Expand All @@ -2281,14 +2287,35 @@ private void OpenTiffToImageSharp(ReadOnlySpan<byte> bytes)
}
}

private void SetTiffCompression(Tiff tiff)
{
Compression tiffCompression = tiff.GetField(TiffTag.COMPRESSION) != null && tiff.GetField(TiffTag.COMPRESSION).Length > 0
? (Compression)tiff.GetField(TiffTag.COMPRESSION)[0].ToInt()
: Compression.NONE;

TiffCompression = tiffCompression switch
{
Compression.CCITTRLE => TiffCompression.Ccitt1D,
Compression.CCITTFAX3 => TiffCompression.CcittGroup3Fax,
Compression.CCITTFAX4 => TiffCompression.CcittGroup4Fax,
Compression.JPEG => TiffCompression.Jpeg,
Compression.OJPEG => TiffCompression.OldJpeg,
Compression.NEXT => TiffCompression.NeXT,
Compression.PACKBITS => TiffCompression.PackBits,
Compression.THUNDERSCAN => TiffCompression.ThunderScan,
Compression.DEFLATE => TiffCompression.Deflate,
_ => TiffCompression.Lzw
};
}

/// <summary>
/// Determines if a TIFF frame contains a thumbnail.
/// </summary>
/// <param name="tif">The <see cref="Tiff"/> which set number of directory to analyze.</param>
/// <param name="tiff">The <see cref="Tiff"/> which set number of directory to analyze.</param>
/// <returns>True if the frame contains a thumbnail, otherwise false.</returns>
private bool IsThumbnail(Tiff tif)
private bool IsThumbnail(Tiff tiff)
{
FieldValue[] subFileTypeFieldValue = tif.GetField(TiffTag.SUBFILETYPE);
FieldValue[] subFileTypeFieldValue = tiff.GetField(TiffTag.SUBFILETYPE);

// 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
Expand Down Expand Up @@ -2319,13 +2346,13 @@ private ReadOnlySpan<byte> PrepareByteArray(Image<Rgba32> bmp, int[] raster, int
return bits;
}

private (int width, int height) SetWidthHeight(Tiff tif, short index, ref int imageWidth, ref int imageHeight)
private (int width, int height) SetWidthHeight(Tiff tiff, short index, ref int imageWidth, ref int imageHeight)
{
// Find the width and height of the image
FieldValue[] value = tif.GetField(TiffTag.IMAGEWIDTH);
FieldValue[] value = tiff.GetField(TiffTag.IMAGEWIDTH);
int width = value[0].ToInt();

value = tif.GetField(TiffTag.IMAGELENGTH);
value = tiff.GetField(TiffTag.IMAGELENGTH);
int height = value[0].ToInt();

if (index == 0)
Expand Down
2 changes: 1 addition & 1 deletion IronSoftware.Drawing/IronSoftware.Drawing.sln
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "job_templates", "job_templa
..\CI\job_templates\test_drawing_libraries.yml = ..\CI\job_templates\test_drawing_libraries.yml
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IronSoftware.Drawing.Common.ConsoleTest", "IronSoftware.Drawing.Common.ConsoleTest\IronSoftware.Drawing.Common.ConsoleTest.csproj", "{A0B0F474-108B-4B60-834B-8FB169743687}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IronSoftware.Drawing.Common.ConsoleTest", "IronSoftware.Drawing.Common.ConsoleTest\IronSoftware.Drawing.Common.ConsoleTest.csproj", "{A0B0F474-108B-4B60-834B-8FB169743687}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down
4 changes: 3 additions & 1 deletion NuGet/IronSoftware.Drawing.nuspec
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ Supports:

For general support and technical inquiries, please email us at: support@ironsoftware.com</description>
<summary>IronSoftware.System.Drawing is an open-source solution for .NET developers to replace System.Drawing.Common with a universal and flexible library.</summary>
<releaseNotes>- Improves memory management when loading large image input</releaseNotes>
<releaseNotes>- Efficient compression significantly reduces output TIFF file size (Up to 80%).
- Automatic compression when creating multi-frame TIFFs.
- Fix the issue of exception error thrown when converting Bitmap to AnyBitmap.</releaseNotes>
<copyright>Copyright © Iron Software 2022-2024</copyright>
<tags>Images, Bitmap, SkiaSharp, SixLabors, BitMiracle, Maui, SVG, TIFF, TIF, GIF, JPEG, PNG, Color, Rectangle, Drawing, C#, VB.NET, ASPX, create, render, generate, standard, netstandard2.0, core, netcore</tags>
<repository type="git" url="https://github.com/iron-software/IronSoftware.Drawing.Common" commit="$commit$" />
Expand Down

0 comments on commit e534614

Please sign in to comment.