Browse Source

Merge branch 'main' into icc-color-conversion

pull/1567/head
James Jackson-South 3 years ago
committed by GitHub
parent
commit
60f3d9d2da
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .gitattributes
  2. 2
      .github/CONTRIBUTING.md
  3. 56
      .github/ISSUE_TEMPLATE/commercial-bug-report.yml
  4. 4
      .github/ISSUE_TEMPLATE/oss-bug-report.yml
  5. 1
      .github/workflows/build-and-test.yml
  6. 15
      ImageSharp.sln
  7. 9
      README.md
  8. 2
      SixLabors.ImageSharp.props
  9. 2
      shared-infrastructure
  10. 10
      src/ImageSharp/Advanced/ParallelRowIterator.cs
  11. 24
      src/ImageSharp/Common/Helpers/Numerics.cs
  12. 5
      src/ImageSharp/Configuration.cs
  13. 2
      src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
  14. 205
      src/ImageSharp/Formats/ImageExtensions.Save.cs
  15. 3
      src/ImageSharp/Formats/ImageExtensions.Save.tt
  16. 4
      src/ImageSharp/Formats/Jpeg/Components/Decoder/JFifMarker.cs
  17. 7
      src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBitReader.cs
  18. 8
      src/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs
  19. 23
      src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs
  20. 31
      src/ImageSharp/Formats/Jpeg/Components/Encoder/ComponentProcessor.cs
  21. 16
      src/ImageSharp/Formats/Pbm/BinaryDecoder.cs
  22. 40
      src/ImageSharp/Formats/Pbm/BinaryEncoder.cs
  23. 2
      src/ImageSharp/Formats/Pbm/BufferedReadStreamExtensions.cs
  24. 2
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  25. 20
      src/ImageSharp/Formats/Qoi/MetadataExtensions.cs
  26. 20
      src/ImageSharp/Formats/Qoi/QoiChannels.cs
  27. 56
      src/ImageSharp/Formats/Qoi/QoiChunk.cs
  28. 22
      src/ImageSharp/Formats/Qoi/QoiColorSpace.cs
  29. 18
      src/ImageSharp/Formats/Qoi/QoiConfigurationModule.cs
  30. 27
      src/ImageSharp/Formats/Qoi/QoiConstants.cs
  31. 43
      src/ImageSharp/Formats/Qoi/QoiDecoder.cs
  32. 291
      src/ImageSharp/Formats/Qoi/QoiDecoderCore.cs
  33. 33
      src/ImageSharp/Formats/Qoi/QoiEncoder.cs
  34. 231
      src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs
  35. 34
      src/ImageSharp/Formats/Qoi/QoiFormat.cs
  36. 45
      src/ImageSharp/Formats/Qoi/QoiHeader.cs
  37. 25
      src/ImageSharp/Formats/Qoi/QoiImageFormatDetector.cs
  38. 40
      src/ImageSharp/Formats/Qoi/QoiMetadata.cs
  39. BIN
      src/ImageSharp/Formats/Qoi/qoi-specification.pdf
  40. 20
      src/ImageSharp/Formats/Tiff/Compression/Decompressors/T6BitReader.cs
  41. 1
      src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs
  42. 2
      src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
  43. 24
      src/ImageSharp/Formats/Webp/Lossy/Vp8Residual.cs
  44. 26
      src/ImageSharp/IO/IFileSystem.cs
  45. 20
      src/ImageSharp/IO/LocalFileSystem.cs
  46. 10
      src/ImageSharp/Image.FromFile.cs
  47. 4
      src/ImageSharp/Image.FromStream.cs
  48. 2
      src/ImageSharp/ImageExtensions.cs
  49. 7
      src/ImageSharp/Image{TPixel}.cs
  50. 2
      src/ImageSharp/PixelFormats/PixelImplementations/Rgb48.cs
  51. 10
      src/ImageSharp/Primitives/LongRational.cs
  52. 162
      src/ImageSharp/Processing/Extensions/Drawing/DrawImageExtensions.cs
  53. 59
      src/ImageSharp/Processing/Processors/Drawing/DrawImageProcessor.cs
  54. 132
      src/ImageSharp/Processing/Processors/Drawing/DrawImageProcessor{TPixelBg,TPixelFg}.cs
  55. 47
      src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor{TPixel}.cs
  56. 4
      src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs
  57. 2
      tests/Directory.Build.targets
  58. 19
      tests/ImageSharp.Benchmarks/Processing/OilPaint.cs
  59. 2
      tests/ImageSharp.Tests/ConfigurationTests.cs
  60. 20
      tests/ImageSharp.Tests/Drawing/DrawImageExtensionsTests.cs
  61. 67
      tests/ImageSharp.Tests/Drawing/DrawImageTests.cs
  62. 24
      tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs
  63. 7
      tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs
  64. 28
      tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
  65. 1
      tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs
  66. 6
      tests/ImageSharp.Tests/Formats/Pbm/PbmEncoderTests.cs
  67. 11
      tests/ImageSharp.Tests/Formats/Pbm/PbmMetadataTests.cs
  68. 45
      tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
  69. 64
      tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
  70. 135
      tests/ImageSharp.Tests/Formats/Qoi/ImageExtensionsTest.cs
  71. 56
      tests/ImageSharp.Tests/Formats/Qoi/QoiDecoderTests.cs
  72. 44
      tests/ImageSharp.Tests/Formats/Qoi/QoiEncoderTests.cs
  73. 6
      tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs
  74. 55
      tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
  75. 235
      tests/ImageSharp.Tests/Formats/WebP/Vp8ResidualTests.cs
  76. 6
      tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs
  77. 39
      tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs
  78. 109
      tests/ImageSharp.Tests/IO/LocalFileSystemTests.cs
  79. 8
      tests/ImageSharp.Tests/Image/ImageSaveTests.cs
  80. 6
      tests/ImageSharp.Tests/Image/ImageTests.ImageLoadTestBase.cs
  81. 7
      tests/ImageSharp.Tests/ImageSharp.Tests.csproj
  82. 8
      tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedMemoryPoolTests.Trim.cs
  83. 45
      tests/ImageSharp.Tests/Numerics/RationalTests.cs
  84. 13
      tests/ImageSharp.Tests/Processing/Processors/Effects/OilPaintTest.cs
  85. BIN
      tests/ImageSharp.Tests/TestDataWebp/Vp8Residual.bin
  86. 1
      tests/ImageSharp.Tests/TestDataWebp/Vp8Residual.json
  87. 44
      tests/ImageSharp.Tests/TestFileSystem.cs
  88. 27
      tests/ImageSharp.Tests/TestImages.cs
  89. 4
      tests/ImageSharp.Tests/TestUtilities/SingleStreamFileSystem.cs
  90. 4
      tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs
  91. 4
      tests/Images/External/ReferenceOutput/DitherTests/ApplyDitherFilterInBox_Rgba32_CalliphoraPartial.png
  92. 4
      tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_Bike_Bayer16x16.png
  93. 4
      tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_Bike_Bayer2x2.png
  94. 4
      tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_Bike_Bayer4x4.png
  95. 4
      tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_Bike_Bayer8x8.png
  96. 4
      tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_Bike_Ordered3x3.png
  97. 4
      tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_CalliphoraPartial_Bayer16x16.png
  98. 4
      tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_CalliphoraPartial_Bayer2x2.png
  99. 4
      tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_CalliphoraPartial_Bayer4x4.png
  100. 4
      tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_CalliphoraPartial_Bayer8x8.png

1
.gitattributes

@ -118,6 +118,7 @@
*.bmp filter=lfs diff=lfs merge=lfs -text
*.gif filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text
*.qoi filter=lfs diff=lfs merge=lfs -text
*.tif filter=lfs diff=lfs merge=lfs -text
*.tiff filter=lfs diff=lfs merge=lfs -text
*.tga filter=lfs diff=lfs merge=lfs -text

2
.github/CONTRIBUTING.md

@ -29,7 +29,6 @@
#### **Running tests and Debugging**
* Expected test output is pulled in as a submodule from the [ImageSharp.Tests.Images repository](https://github.com/SixLabors/Imagesharp.Tests.Images/tree/main/ReferenceOutput). To succesfully run tests, make sure that you have updated the submodules!
* Debugging (running tests in Debug mode) is only supported on .NET Core 2.1+, because of JIT Code Generation bugs like [dotnet/coreclr#16443](https://github.com/dotnet/coreclr/issues/16443) or [dotnet/coreclr#20657](https://github.com/dotnet/coreclr/issues/20657)
#### **Do you have questions about consuming the library or the source code?**
@ -37,7 +36,6 @@
#### Code of Conduct
This project has adopted the code of conduct defined by the [Contributor Covenant](https://contributor-covenant.org/) to clarify expected behavior in our community.
For more information, see the [.NET Foundation Code of Conduct](https://dotnetfoundation.org/code-of-conduct).
And please remember. SixLabors.ImageSharp is the work of a very, very, small number of developers who struggle balancing time to contribute to the project with family time and work commitments. We encourage you to pitch in and help make our vision of simple accessible image processing available to all. Open Source can only exist with your help.

56
.github/ISSUE_TEMPLATE/commercial-bug-report.yml

@ -1,56 +0,0 @@
name: "Commercial License : Bug Report"
description: |
Create a report to help us improve the project. For Commercial License holders only.
Please contact help@sixlabors.com for issues requiring private support.
labels: ["commercial", "needs triage"]
body:
- type: checkboxes
attributes:
label: Prerequisites
options:
- label: I have bought a Commercial License
required: true
- label: I have written a descriptive issue title
required: true
- label: I have verified that I am running the latest version of ImageSharp
required: true
- label: I have verified if the problem exist in both `DEBUG` and `RELEASE` mode
required: true
- label: I have searched [open](https://github.com/SixLabors/ImageSharp/issues) and [closed](https://github.com/SixLabors/ImageSharp/issues?q=is%3Aissue+is%3Aclosed) issues to ensure it has not already been reported
required: true
- type: input
attributes:
label: ImageSharp version
validations:
required: true
- type: input
attributes:
label: Other ImageSharp packages and versions
validations:
required: true
- type: input
attributes:
label: Environment (Operating system, version and so on)
validations:
required: true
- type: input
attributes:
label: .NET Framework version
validations:
required: true
- type: textarea
attributes:
label: Description
description: A description of the bug
validations:
required: true
- type: textarea
attributes:
label: Steps to Reproduce
description: List of steps, sample code, failing test or link to a project that reproduces the behavior. Make sure you place a stack trace inside a code (```) block to avoid linking unrelated issues.
validations:
required: true
- type: textarea
attributes:
label: Images
description: Please upload images that can be used to reproduce issues in the area below. If the file type is not supported the file can be zipped and then uploaded instead.

4
.github/ISSUE_TEMPLATE/oss-bug-report.yml

@ -1,5 +1,5 @@
name: "OSS : Bug Report"
description: Create a report to help us improve the project. OSS Issues are not guaranteed to be triaged.
name: "Bug Report"
description: Create a report to help us improve the project. Issues are not guaranteed to be triaged.
labels: ["needs triage"]
body:
- type: checkboxes

1
.github/workflows/build-and-test.yml

@ -9,6 +9,7 @@ on:
pull_request:
branches:
- main
- release/*
types: [ labeled, opened, synchronize, reopened ]
jobs:
Build:

15
ImageSharp.sln

@ -18,6 +18,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_root", "_root", "{C317F1B1
Directory.Build.targets = Directory.Build.targets
LICENSE = LICENSE
README.md = README.md
SixLabors.ImageSharp.props = SixLabors.ImageSharp.props
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{1799C43E-5C54-4A8F-8D64-B1475241DB0D}"
@ -28,7 +29,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{1799
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ISSUE_TEMPLATE", "ISSUE_TEMPLATE", "{FBE8C1AD-5AEC-4514-9B64-091D8E145865}"
ProjectSection(SolutionItems) = preProject
.github\ISSUE_TEMPLATE\commercial-bug-report.yml = .github\ISSUE_TEMPLATE\commercial-bug-report.yml
.github\ISSUE_TEMPLATE\config.yml = .github\ISSUE_TEMPLATE\config.yml
.github\ISSUE_TEMPLATE\oss-bug-report.yml = .github\ISSUE_TEMPLATE\oss-bug-report.yml
EndProjectSection
@ -647,6 +647,18 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tga", "Tga", "{5DFC394F-136
tests\Images\Input\Tga\targa_8bit_rle.tga = tests\Images\Input\Tga\targa_8bit_rle.tga
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Qoi", "Qoi", "{E801B508-4935-41CD-BA85-CF11BFF55A45}"
ProjectSection(SolutionItems) = preProject
tests\Images\Input\Qoi\dice.qoi = tests\Images\Input\Qoi\dice.qoi
tests\Images\Input\Qoi\edgecase.qoi = tests\Images\Input\Qoi\edgecase.qoi
tests\Images\Input\Qoi\kodim10.qoi = tests\Images\Input\Qoi\kodim10.qoi
tests\Images\Input\Qoi\kodim23.qoi = tests\Images\Input\Qoi\kodim23.qoi
tests\Images\Input\Qoi\qoi_logo.qoi = tests\Images\Input\Qoi\qoi_logo.qoi
tests\Images\Input\Qoi\testcard.qoi = tests\Images\Input\Qoi\testcard.qoi
tests\Images\Input\Qoi\testcard_rgba.qoi = tests\Images\Input\Qoi\testcard_rgba.qoi
tests\Images\Input\Qoi\wikipedia_008.qoi = tests\Images\Input\Qoi\wikipedia_008.qoi
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -699,6 +711,7 @@ Global
{FC527290-2F22-432C-B77B-6E815726B02C} = {56801022-D71A-4FBE-BC5B-CBA08E2284EC}
{670DD46C-82E9-499A-B2D2-00A802ED0141} = {E1C42A6F-913B-4A7B-B1A8-2BB62843B254}
{5DFC394F-136F-4B76-9BCA-3BA786515EFC} = {9DA226A1-8656-49A8-A58A-A8B5C081AD66}
{E801B508-4935-41CD-BA85-CF11BFF55A45} = {9DA226A1-8656-49A8-A58A-A8B5C081AD66}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5F8B9D1F-CD8B-4CC5-8216-D531E25BD795}

9
README.md

@ -101,6 +101,8 @@ git submodule update --init --recursive
Please... Spread the word, contribute algorithms, submit performance improvements, unit tests, no input is too little. Make sure to read our [Contribution Guide](https://github.com/SixLabors/ImageSharp/blob/main/.github/CONTRIBUTING.md) before opening a PR.
Useful tools for development and links to specifications can be found in our wikipage: [Useful-tools-and-links](https://github.com/SixLabors/ImageSharp/wiki/Useful-tools-and-links).
## The ImageSharp Team
- [James Jackson-South](https://github.com/jimbobsquarepants)
@ -109,6 +111,11 @@ Please... Spread the word, contribute algorithms, submit performance improvement
- [Scott Williams](https://github.com/tocsoft)
- [Brian Popow](https://github.com/brianpopow)
---
<div>
<a href="https://www.jetbrains.com/?from=ImageSharp" align="right"><img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.svg" alt="JetBrains" class="logo-footer" width="72" align="left"></a>
<br/>
Special thanks to [JetBrains](https://www.jetbrains.com/?from=ImageSharp) for supporting us with open-source licenses for their IDEs.
</div>

2
SixLabors.ImageSharp.props

@ -2,7 +2,7 @@
<Project>
<!--Add common namespaces to implicit global usings if enabled.-->
<ItemGroup Condition="'$(ImplicitUsings)'=='enable' OR '$(ImplicitUsings)'=='true'">
<ItemGroup Condition="'$(UseImageSharp)'=='enable' OR '$(UseImageSharp)'=='true'">
<Using Include="SixLabors.ImageSharp" />
<Using Include="SixLabors.ImageSharp.PixelFormats" />
<Using Include="SixLabors.ImageSharp.Processing" />

2
shared-infrastructure

@ -1 +1 @@
Subproject commit 9a6cf00d9a3d482bb08211dd8309f4724a2735cb
Subproject commit 353b9afe32a8000410312d17263407cd7bb82d19

10
src/ImageSharp/Advanced/ParallelRowIterator.cs

@ -50,7 +50,7 @@ public static partial class ParallelRowIterator
int width = rectangle.Width;
int height = rectangle.Height;
int maxSteps = DivideCeil(width * height, parallelSettings.MinimumPixelsProcessedPerTask);
int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
// Avoid TPL overhead in this trivial case:
@ -115,7 +115,7 @@ public static partial class ParallelRowIterator
int width = rectangle.Width;
int height = rectangle.Height;
int maxSteps = DivideCeil(width * height, parallelSettings.MinimumPixelsProcessedPerTask);
int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
MemoryAllocator allocator = parallelSettings.MemoryAllocator;
int bufferLength = Unsafe.AsRef(operation).GetRequiredBufferLength(rectangle);
@ -180,7 +180,7 @@ public static partial class ParallelRowIterator
int width = rectangle.Width;
int height = rectangle.Height;
int maxSteps = DivideCeil(width * height, parallelSettings.MinimumPixelsProcessedPerTask);
int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
// Avoid TPL overhead in this trivial case:
@ -242,7 +242,7 @@ public static partial class ParallelRowIterator
int width = rectangle.Width;
int height = rectangle.Height;
int maxSteps = DivideCeil(width * height, parallelSettings.MinimumPixelsProcessedPerTask);
int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
MemoryAllocator allocator = parallelSettings.MemoryAllocator;
int bufferLength = Unsafe.AsRef(operation).GetRequiredBufferLength(rectangle);
@ -270,7 +270,7 @@ public static partial class ParallelRowIterator
}
[MethodImpl(InliningOptions.ShortMethod)]
private static int DivideCeil(int dividend, int divisor) => 1 + ((dividend - 1) / divisor);
private static int DivideCeil(long dividend, int divisor) => (int)Math.Min(1 + ((dividend - 1) / divisor), int.MaxValue);
private static void ValidateRectangle(Rectangle rectangle)
{

24
src/ImageSharp/Common/Helpers/Numerics.cs

@ -73,6 +73,30 @@ internal static class Numerics
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static nint Modulo8(nint x) => x & 7;
/// <summary>
/// Calculates <paramref name="x"/> % 64
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int Modulo64(int x) => x & 63;
/// <summary>
/// Calculates <paramref name="x"/> % 64
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static nint Modulo64(nint x) => x & 63;
/// <summary>
/// Calculates <paramref name="x"/> % 256
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int Modulo256(int x) => x & 255;
/// <summary>
/// Calculates <paramref name="x"/> % 256
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static nint Modulo256(nint x) => x & 255;
/// <summary>
/// Fast (x mod m) calculator, with the restriction that
/// <paramref name="m"/> should be power of 2.

5
src/ImageSharp/Configuration.cs

@ -8,6 +8,7 @@ using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Pbm;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Qoi;
using SixLabors.ImageSharp.Formats.Tga;
using SixLabors.ImageSharp.Formats.Tiff;
using SixLabors.ImageSharp.Formats.Webp;
@ -212,6 +213,7 @@ public sealed class Configuration
/// <see cref="TgaConfigurationModule"/>.
/// <see cref="TiffConfigurationModule"/>.
/// <see cref="WebpConfigurationModule"/>.
/// <see cref="QoiConfigurationModule"/>.
/// </summary>
/// <returns>The default configuration of <see cref="Configuration"/>.</returns>
internal static Configuration CreateDefaultInstance() => new(
@ -222,5 +224,6 @@ public sealed class Configuration
new PbmConfigurationModule(),
new TgaConfigurationModule(),
new TiffConfigurationModule(),
new WebpConfigurationModule());
new WebpConfigurationModule(),
new QoiConfigurationModule());
}

2
src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs

@ -668,7 +668,7 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
if (quantizedPixelRow.Length % 8 != 0)
{
int startIdx = quantizedPixelRow.Length - 7;
int startIdx = quantizedPixelRow.Length - (quantizedPixelRow.Length % 8);
endIdx = quantizedPixelRow.Length;
Write1BitPalette(stream, startIdx, endIdx, quantizedPixelRow);
}

205
src/ImageSharp/Formats/ImageExtensions.Save.cs

@ -9,9 +9,10 @@ using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Pbm;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Qoi;
using SixLabors.ImageSharp.Formats.Tga;
using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Formats.Tiff;
using SixLabors.ImageSharp.Formats.Webp;
namespace SixLabors.ImageSharp;
@ -531,47 +532,47 @@ public static partial class ImageExtensions
cancellationToken);
/// <summary>
/// Saves the image to the given stream with the Tga format.
/// Saves the image to the given stream with the Qoi format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
public static void SaveAsTga(this Image source, string path) => SaveAsTga(source, path, default);
public static void SaveAsQoi(this Image source, string path) => SaveAsQoi(source, path, default);
/// <summary>
/// Saves the image to the given stream with the Tga format.
/// Saves the image to the given stream with the Qoi format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsTgaAsync(this Image source, string path) => SaveAsTgaAsync(source, path, default);
public static Task SaveAsQoiAsync(this Image source, string path) => SaveAsQoiAsync(source, path, default);
/// <summary>
/// Saves the image to the given stream with the Tga format.
/// Saves the image to the given stream with the Qoi format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsTgaAsync(this Image source, string path, CancellationToken cancellationToken)
=> SaveAsTgaAsync(source, path, default, cancellationToken);
public static Task SaveAsQoiAsync(this Image source, string path, CancellationToken cancellationToken)
=> SaveAsQoiAsync(source, path, default, cancellationToken);
/// <summary>
/// Saves the image to the given stream with the Tga format.
/// Saves the image to the given stream with the Qoi format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
public static void SaveAsTga(this Image source, string path, TgaEncoder encoder) =>
public static void SaveAsQoi(this Image source, string path, QoiEncoder encoder) =>
source.Save(
path,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(TgaFormat.Instance));
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(QoiFormat.Instance));
/// <summary>
/// Saves the image to the given stream with the Tga format.
/// Saves the image to the given stream with the Qoi format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
@ -579,46 +580,46 @@ public static partial class ImageExtensions
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsTgaAsync(this Image source, string path, TgaEncoder encoder, CancellationToken cancellationToken = default)
public static Task SaveAsQoiAsync(this Image source, string path, QoiEncoder encoder, CancellationToken cancellationToken = default)
=> source.SaveAsync(
path,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(TgaFormat.Instance),
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(QoiFormat.Instance),
cancellationToken);
/// <summary>
/// Saves the image to the given stream with the Tga format.
/// Saves the image to the given stream with the Qoi format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
public static void SaveAsTga(this Image source, Stream stream)
=> SaveAsTga(source, stream, default);
public static void SaveAsQoi(this Image source, Stream stream)
=> SaveAsQoi(source, stream, default);
/// <summary>
/// Saves the image to the given stream with the Tga format.
/// Saves the image to the given stream with the Qoi format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsTgaAsync(this Image source, Stream stream, CancellationToken cancellationToken = default)
=> SaveAsTgaAsync(source, stream, default, cancellationToken);
public static Task SaveAsQoiAsync(this Image source, Stream stream, CancellationToken cancellationToken = default)
=> SaveAsQoiAsync(source, stream, default, cancellationToken);
/// <summary>
/// Saves the image to the given stream with the Tga format.
/// Saves the image to the given stream with the Qoi format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
public static void SaveAsTga(this Image source, Stream stream, TgaEncoder encoder)
public static void SaveAsQoi(this Image source, Stream stream, QoiEncoder encoder)
=> source.Save(
stream,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(TgaFormat.Instance));
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(QoiFormat.Instance));
/// <summary>
/// Saves the image to the given stream with the Tga format.
/// Saves the image to the given stream with the Qoi format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
@ -626,54 +627,54 @@ public static partial class ImageExtensions
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsTgaAsync(this Image source, Stream stream, TgaEncoder encoder, CancellationToken cancellationToken = default)
public static Task SaveAsQoiAsync(this Image source, Stream stream, QoiEncoder encoder, CancellationToken cancellationToken = default)
=> source.SaveAsync(
stream,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(TgaFormat.Instance),
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(QoiFormat.Instance),
cancellationToken);
/// <summary>
/// Saves the image to the given stream with the Webp format.
/// Saves the image to the given stream with the Tga format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
public static void SaveAsWebp(this Image source, string path) => SaveAsWebp(source, path, default);
public static void SaveAsTga(this Image source, string path) => SaveAsTga(source, path, default);
/// <summary>
/// Saves the image to the given stream with the Webp format.
/// Saves the image to the given stream with the Tga format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsWebpAsync(this Image source, string path) => SaveAsWebpAsync(source, path, default);
public static Task SaveAsTgaAsync(this Image source, string path) => SaveAsTgaAsync(source, path, default);
/// <summary>
/// Saves the image to the given stream with the Webp format.
/// Saves the image to the given stream with the Tga format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsWebpAsync(this Image source, string path, CancellationToken cancellationToken)
=> SaveAsWebpAsync(source, path, default, cancellationToken);
public static Task SaveAsTgaAsync(this Image source, string path, CancellationToken cancellationToken)
=> SaveAsTgaAsync(source, path, default, cancellationToken);
/// <summary>
/// Saves the image to the given stream with the Webp format.
/// Saves the image to the given stream with the Tga format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
public static void SaveAsWebp(this Image source, string path, WebpEncoder encoder) =>
public static void SaveAsTga(this Image source, string path, TgaEncoder encoder) =>
source.Save(
path,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(WebpFormat.Instance));
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(TgaFormat.Instance));
/// <summary>
/// Saves the image to the given stream with the Webp format.
/// Saves the image to the given stream with the Tga format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
@ -681,46 +682,46 @@ public static partial class ImageExtensions
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsWebpAsync(this Image source, string path, WebpEncoder encoder, CancellationToken cancellationToken = default)
public static Task SaveAsTgaAsync(this Image source, string path, TgaEncoder encoder, CancellationToken cancellationToken = default)
=> source.SaveAsync(
path,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(WebpFormat.Instance),
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(TgaFormat.Instance),
cancellationToken);
/// <summary>
/// Saves the image to the given stream with the Webp format.
/// Saves the image to the given stream with the Tga format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
public static void SaveAsWebp(this Image source, Stream stream)
=> SaveAsWebp(source, stream, default);
public static void SaveAsTga(this Image source, Stream stream)
=> SaveAsTga(source, stream, default);
/// <summary>
/// Saves the image to the given stream with the Webp format.
/// Saves the image to the given stream with the Tga format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsWebpAsync(this Image source, Stream stream, CancellationToken cancellationToken = default)
=> SaveAsWebpAsync(source, stream, default, cancellationToken);
public static Task SaveAsTgaAsync(this Image source, Stream stream, CancellationToken cancellationToken = default)
=> SaveAsTgaAsync(source, stream, default, cancellationToken);
/// <summary>
/// Saves the image to the given stream with the Webp format.
/// Saves the image to the given stream with the Tga format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
public static void SaveAsWebp(this Image source, Stream stream, WebpEncoder encoder)
public static void SaveAsTga(this Image source, Stream stream, TgaEncoder encoder)
=> source.Save(
stream,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(WebpFormat.Instance));
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(TgaFormat.Instance));
/// <summary>
/// Saves the image to the given stream with the Webp format.
/// Saves the image to the given stream with the Tga format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
@ -728,10 +729,10 @@ public static partial class ImageExtensions
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsWebpAsync(this Image source, Stream stream, WebpEncoder encoder, CancellationToken cancellationToken = default)
public static Task SaveAsTgaAsync(this Image source, Stream stream, TgaEncoder encoder, CancellationToken cancellationToken = default)
=> source.SaveAsync(
stream,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(WebpFormat.Instance),
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(TgaFormat.Instance),
cancellationToken);
/// <summary>
@ -836,4 +837,106 @@ public static partial class ImageExtensions
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(TiffFormat.Instance),
cancellationToken);
/// <summary>
/// Saves the image to the given stream with the Webp format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
public static void SaveAsWebp(this Image source, string path) => SaveAsWebp(source, path, default);
/// <summary>
/// Saves the image to the given stream with the Webp format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsWebpAsync(this Image source, string path) => SaveAsWebpAsync(source, path, default);
/// <summary>
/// Saves the image to the given stream with the Webp format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsWebpAsync(this Image source, string path, CancellationToken cancellationToken)
=> SaveAsWebpAsync(source, path, default, cancellationToken);
/// <summary>
/// Saves the image to the given stream with the Webp format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
public static void SaveAsWebp(this Image source, string path, WebpEncoder encoder) =>
source.Save(
path,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(WebpFormat.Instance));
/// <summary>
/// Saves the image to the given stream with the Webp format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsWebpAsync(this Image source, string path, WebpEncoder encoder, CancellationToken cancellationToken = default)
=> source.SaveAsync(
path,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(WebpFormat.Instance),
cancellationToken);
/// <summary>
/// Saves the image to the given stream with the Webp format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
public static void SaveAsWebp(this Image source, Stream stream)
=> SaveAsWebp(source, stream, default);
/// <summary>
/// Saves the image to the given stream with the Webp format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsWebpAsync(this Image source, Stream stream, CancellationToken cancellationToken = default)
=> SaveAsWebpAsync(source, stream, default, cancellationToken);
/// <summary>
/// Saves the image to the given stream with the Webp format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
public static void SaveAsWebp(this Image source, Stream stream, WebpEncoder encoder)
=> source.Save(
stream,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(WebpFormat.Instance));
/// <summary>
/// Saves the image to the given stream with the Webp format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsWebpAsync(this Image source, Stream stream, WebpEncoder encoder, CancellationToken cancellationToken = default)
=> source.SaveAsync(
stream,
encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(WebpFormat.Instance),
cancellationToken);
}

3
src/ImageSharp/Formats/ImageExtensions.Save.tt

@ -14,9 +14,10 @@ using SixLabors.ImageSharp.Advanced;
"Jpeg",
"Pbm",
"Png",
"Qoi",
"Tga",
"Webp",
"Tiff",
"Webp",
};
foreach (string fmt in formats)

4
src/ImageSharp/Formats/Jpeg/Components/Decoder/JFifMarker.cs

@ -71,7 +71,9 @@ internal readonly struct JFifMarker : IEquatable<JFifMarker>
/// <param name="marker">The marker to return.</param>
public static bool TryParse(ReadOnlySpan<byte> bytes, out JFifMarker marker)
{
if (ProfileResolver.IsProfile(bytes, ProfileResolver.JFifMarker))
// Some images incorrectly use JFXX as the App0 marker (Issue 2478)
if (ProfileResolver.IsProfile(bytes, ProfileResolver.JFifMarker)
|| ProfileResolver.IsProfile(bytes, ProfileResolver.JFxxMarker))
{
byte majorVersion = bytes[5];
byte minorVersion = bytes[6];

7
src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBitReader.cs

@ -212,7 +212,12 @@ internal struct JpegBitReader
private int ReadStream()
{
int value = this.badData ? 0 : this.stream.ReadByte();
if (value == -1)
// We've encountered the end of the file stream which means there's no EOI marker or the marker has been read
// during decoding of the SOS marker.
// When reading individual bits 'badData' simply means we have hit a marker, When data is '0' and the stream is exhausted
// we know we have hit the EOI and completed decoding the scan buffer.
if (value == -1 || (this.badData && this.data == 0 && this.stream.Position >= this.stream.Length))
{
// We've encountered the end of the file stream which means there's no EOI marker
// in the image or the SOS marker has the wrong dimensions set.

8
src/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs

@ -16,6 +16,14 @@ internal static class ProfileResolver
(byte)'J', (byte)'F', (byte)'I', (byte)'F', (byte)'\0'
};
/// <summary>
/// Gets the JFXX specific markers.
/// </summary>
public static ReadOnlySpan<byte> JFxxMarker => new[]
{
(byte)'J', (byte)'F', (byte)'X', (byte)'X', (byte)'\0'
};
/// <summary>
/// Gets the ICC specific markers.
/// </summary>

23
src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs

@ -184,34 +184,39 @@ internal class SpectralConverter<TPixel> : SpectralConverter, IDisposable
MemoryAllocator allocator = this.Configuration.MemoryAllocator;
// color converter from RGB to TPixel
// Color converter from RGB to TPixel
JpegColorConverterBase converter = this.GetColorConverter(this.frame, this.jpegData);
this.colorConverter = converter;
// resulting image size
// Resulting image size
Size pixelSize = CalculateResultingImageSize(this.frame.PixelSize, this.targetSize, out int blockPixelSize);
// iteration data
// Iteration data
int majorBlockWidth = this.frame.Components.Max((component) => component.SizeInBlocks.Width);
int majorVerticalSamplingFactor = this.frame.Components.Max((component) => component.SamplingFactors.Height);
this.pixelRowsPerStep = majorVerticalSamplingFactor * blockPixelSize;
// pixel buffer for resulting image
// Pixel buffer for resulting image
this.pixelBuffer = allocator.Allocate2D<TPixel>(
pixelSize.Width,
pixelSize.Height,
this.Configuration.PreferContiguousImageBuffers);
this.paddedProxyPixelRow = allocator.Allocate<TPixel>(pixelSize.Width + 3);
// component processors from spectral to RGB
// Component processors from spectral to RGB
int bufferWidth = majorBlockWidth * blockPixelSize;
int batchSize = converter.ElementsPerBatch;
int batchRemainder = bufferWidth & (batchSize - 1);
Size postProcessorBufferSize = new(bufferWidth + (batchSize - batchRemainder), this.pixelRowsPerStep);
// Converters process pixels in batches and require target buffer size to be divisible by a batch size
// Corner case: image size including jpeg padding is already divisible by a batch size or remainder == 0
int elementsPerBatch = converter.ElementsPerBatch;
int batchRemainder = bufferWidth & (elementsPerBatch - 1);
int widthComplementaryValue = batchRemainder == 0 ? 0 : elementsPerBatch - batchRemainder;
Size postProcessorBufferSize = new(bufferWidth + widthComplementaryValue, this.pixelRowsPerStep);
this.componentProcessors = this.CreateComponentProcessors(this.frame, this.jpegData, blockPixelSize, postProcessorBufferSize);
// single 'stride' rgba32 buffer for conversion between spectral and TPixel
// Single 'stride' rgba32 buffer for conversion between spectral and TPixel
this.rgbBuffer = allocator.Allocate<byte>(pixelSize.Width * 3);
}

31
src/ImageSharp/Formats/Jpeg/Components/Encoder/ComponentProcessor.cs

@ -5,6 +5,7 @@ using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.Arm;
using System.Runtime.Intrinsics.X86;
using SixLabors.ImageSharp.Memory;
@ -122,12 +123,26 @@ internal class ComponentProcessor : IDisposable
ref Vector256<float> sourceVectorRef = ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(source));
// Spans are guaranteed to be multiple of 8 so no extra 'remainder' steps are needed
DebugGuard.IsTrue(source.Length % 8 == 0, "source must be multiple of 8");
nuint count = source.Vector256Count<float>();
for (nuint i = 0; i < count; i++)
{
Unsafe.Add(ref targetVectorRef, i) = Avx.Add(Unsafe.Add(ref targetVectorRef, i), Unsafe.Add(ref sourceVectorRef, i));
}
}
else if (AdvSimd.IsSupported)
{
ref Vector128<float> targetVectorRef = ref Unsafe.As<float, Vector128<float>>(ref MemoryMarshal.GetReference(target));
ref Vector128<float> sourceVectorRef = ref Unsafe.As<float, Vector128<float>>(ref MemoryMarshal.GetReference(source));
// Spans are guaranteed to be multiple of 8 so no extra 'remainder' steps are needed
DebugGuard.IsTrue(source.Length % 8 == 0, "source must be multiple of 8");
nuint count = source.Vector128Count<float>();
for (nuint i = 0; i < count; i++)
{
Unsafe.Add(ref targetVectorRef, i) = AdvSimd.Add(Unsafe.Add(ref targetVectorRef, i), Unsafe.Add(ref sourceVectorRef, i));
}
}
else
{
ref Vector<float> targetVectorRef = ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(target));
@ -200,13 +215,27 @@ internal class ComponentProcessor : IDisposable
ref Vector256<float> targetVectorRef = ref Unsafe.As<float, Vector256<float>>(ref MemoryMarshal.GetReference(target));
// Spans are guaranteed to be multiple of 8 so no extra 'remainder' steps are needed
DebugGuard.IsTrue(target.Length % 8 == 0, "target must be multiple of 8");
nuint count = target.Vector256Count<float>();
var multiplierVector = Vector256.Create(multiplier);
Vector256<float> multiplierVector = Vector256.Create(multiplier);
for (nuint i = 0; i < count; i++)
{
Unsafe.Add(ref targetVectorRef, i) = Avx.Multiply(Unsafe.Add(ref targetVectorRef, i), multiplierVector);
}
}
else if (AdvSimd.IsSupported)
{
ref Vector128<float> targetVectorRef = ref Unsafe.As<float, Vector128<float>>(ref MemoryMarshal.GetReference(target));
// Spans are guaranteed to be multiple of 8 so no extra 'remainder' steps are needed
DebugGuard.IsTrue(target.Length % 8 == 0, "target must be multiple of 8");
nuint count = target.Vector128Count<float>();
Vector128<float> multiplierVector = Vector128.Create(multiplier);
for (nuint i = 0; i < count; i++)
{
Unsafe.Add(ref targetVectorRef, i) = AdvSimd.Multiply(Unsafe.Add(ref targetVectorRef, i), multiplierVector);
}
}
else
{
ref Vector<float> targetVectorRef = ref Unsafe.As<float, Vector<float>>(ref MemoryMarshal.GetReference(target));

16
src/ImageSharp/Formats/Pbm/BinaryDecoder.cs

@ -152,7 +152,6 @@ internal class BinaryDecoder
{
int width = pixels.Width;
int height = pixels.Height;
int startBit = 0;
MemoryAllocator allocator = configuration.MemoryAllocator;
using IMemoryOwner<L8> row = allocator.Allocate<L8>(width);
Span<L8> rowSpan = row.GetSpan();
@ -162,23 +161,12 @@ internal class BinaryDecoder
for (int x = 0; x < width;)
{
int raw = stream.ReadByte();
int bit = startBit;
startBit = 0;
for (; bit < 8; bit++)
int stopBit = Math.Min(8, width - x);
for (int bit = 0; bit < stopBit; bit++)
{
bool bitValue = (raw & (0x80 >> bit)) != 0;
rowSpan[x] = bitValue ? black : white;
x++;
if (x == width)
{
startBit = (bit + 1) & 7; // Round off to below 8.
if (startBit != 0)
{
stream.Seek(-1, System.IO.SeekOrigin.Current);
}
break;
}
}
}

40
src/ImageSharp/Formats/Pbm/BinaryEncoder.cs

@ -33,10 +33,14 @@ internal class BinaryEncoder
{
WriteGrayscale(configuration, stream, image);
}
else
else if (componentType == PbmComponentType.Short)
{
WriteWideGrayscale(configuration, stream, image);
}
else
{
throw new ImageFormatException("Component type not supported for Grayscale PBM.");
}
}
else if (colorType == PbmColorType.Rgb)
{
@ -44,14 +48,25 @@ internal class BinaryEncoder
{
WriteRgb(configuration, stream, image);
}
else
else if (componentType == PbmComponentType.Short)
{
WriteWideRgb(configuration, stream, image);
}
else
{
throw new ImageFormatException("Component type not supported for Color PBM.");
}
}
else
{
WriteBlackAndWhite(configuration, stream, image);
if (componentType == PbmComponentType.Bit)
{
WriteBlackAndWhite(configuration, stream, image);
}
else
{
throw new ImageFormatException("Component type not supported for Black & White PBM.");
}
}
}
@ -164,8 +179,6 @@ internal class BinaryEncoder
using IMemoryOwner<L8> row = allocator.Allocate<L8>(width);
Span<L8> rowSpan = row.GetSpan();
int previousValue = 0;
int startBit = 0;
for (int y = 0; y < height; y++)
{
Span<TPixel> pixelSpan = pixelBuffer.DangerousGetRowSpan(y);
@ -177,8 +190,9 @@ internal class BinaryEncoder
for (int x = 0; x < width;)
{
int value = previousValue;
for (int i = startBit; i < 8; i++)
int value = 0;
int stopBit = Math.Min(8, width - x);
for (int i = 0; i < stopBit; i++)
{
if (rowSpan[x].PackedValue < 128)
{
@ -186,19 +200,9 @@ internal class BinaryEncoder
}
x++;
if (x == width)
{
previousValue = value;
startBit = (i + 1) & 7; // Round off to below 8.
break;
}
}
if (startBit == 0)
{
stream.WriteByte((byte)value);
previousValue = 0;
}
stream.WriteByte((byte)value);
}
}
}

2
src/ImageSharp/Formats/Pbm/BufferedReadStreamExtensions.cs

@ -28,7 +28,7 @@ internal static class BufferedReadStreamExtensions
{
innerValue = stream.ReadByte();
}
while (innerValue != 0x0a);
while (innerValue is not 0x0a and not -0x1);
// Continue searching for whitespace.
val = innerValue;

2
src/ImageSharp/Formats/Png/PngEncoderCore.cs

@ -34,7 +34,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable
private readonly MemoryAllocator memoryAllocator;
/// <summary>
/// The configuration instance for the decoding operation.
/// The configuration instance for the encoding operation.
/// </summary>
private readonly Configuration configuration;

20
src/ImageSharp/Formats/Qoi/MetadataExtensions.cs

@ -0,0 +1,20 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Qoi;
using SixLabors.ImageSharp.Metadata;
namespace SixLabors.ImageSharp;
/// <summary>
/// Extension methods for the <see cref="ImageMetadata"/> type.
/// </summary>
public static partial class MetadataExtensions
{
/// <summary>
/// Gets the qoi format specific metadata for the image.
/// </summary>
/// <param name="metadata">The metadata this method extends.</param>
/// <returns>The <see cref="QoiMetadata"/>.</returns>
public static QoiMetadata GetQoiMetadata(this ImageMetadata metadata) => metadata.GetFormatMetadata(QoiFormat.Instance);
}

20
src/ImageSharp/Formats/Qoi/QoiChannels.cs

@ -0,0 +1,20 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Qoi;
/// <summary>
/// Provides enumeration of available QOI color channels.
/// </summary>
public enum QoiChannels
{
/// <summary>
/// Each pixel is an R,G,B triple.
/// </summary>
Rgb = 3,
/// <summary>
/// Each pixel is an R,G,B triple, followed by an alpha sample.
/// </summary>
Rgba = 4
}

56
src/ImageSharp/Formats/Qoi/QoiChunk.cs

@ -0,0 +1,56 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Qoi;
/// <summary>
/// Enum that contains the operations that encoder and decoder must process, written
/// in binary to be easier to compare them in the reference
/// </summary>
internal enum QoiChunk
{
/// <summary>
/// Indicates that the operation is QOI_OP_RGB where the RGB values are written
/// in one byte each one after this marker
/// </summary>
QoiOpRgb = 0b11111110,
/// <summary>
/// Indicates that the operation is QOI_OP_RGBA where the RGBA values are written
/// in one byte each one after this marker
/// </summary>
QoiOpRgba = 0b11111111,
/// <summary>
/// Indicates that the operation is QOI_OP_INDEX where one byte contains a 2-bit
/// marker (0b00) followed by an index on the previously seen pixels array 0..63
/// </summary>
QoiOpIndex = 0b00000000,
/// <summary>
/// Indicates that the operation is QOI_OP_DIFF where one byte contains a 2-bit
/// marker (0b01) followed by 2-bit differences in red, green and blue channel
/// with the previous pixel with a bias of 2 (-2..1)
/// </summary>
QoiOpDiff = 0b01000000,
/// <summary>
/// Indicates that the operation is QOI_OP_LUMA where one byte contains a 2-bit
/// marker (0b01) followed by a 6-bits number that indicates the difference of
/// the green channel with the previous pixel. Then another byte that contains
/// a 4-bit number that indicates the difference of the red channel minus the
/// previous difference, and another 4-bit number that indicates the difference
/// of the blue channel minus the green difference
/// Example: 0b10[6-bits diff green] 0b[6-bits dr-dg][6-bits db-dg]
/// dr_dg = (cur_px.r - prev_px.r) - (cur_px.g - prev_px.g)
/// db_dg = (cur_px.b - prev_px.b) - (cur_px.g - prev_px.g)
/// </summary>
QoiOpLuma = 0b10000000,
/// <summary>
/// Indicates that the operation is QOI_OP_RUN where one byte contains a 2-bit
/// marker (0b11) followed by a 6-bits number that indicates the times that the
/// previous pixel is repeated
/// </summary>
QoiOpRun = 0b11000000
}

22
src/ImageSharp/Formats/Qoi/QoiColorSpace.cs

@ -0,0 +1,22 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
// ReSharper disable InconsistentNaming
// ReSharper disable IdentifierTypo
namespace SixLabors.ImageSharp.Formats.Qoi;
/// <summary>
/// Enum for the different QOI color spaces.
/// </summary>
public enum QoiColorSpace
{
/// <summary>
/// sRGB color space with linear alpha value
/// </summary>
SrgbWithLinearAlpha,
/// <summary>
/// All the values in the color space are linear
/// </summary>
AllChannelsLinear
}

18
src/ImageSharp/Formats/Qoi/QoiConfigurationModule.cs

@ -0,0 +1,18 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Qoi;
/// <summary>
/// Registers the image encoders, decoders and mime type detectors for the qoi format.
/// </summary>
public sealed class QoiConfigurationModule : IImageFormatConfigurationModule
{
/// <inheritdoc/>
public void Configure(Configuration configuration)
{
configuration.ImageFormatsManager.SetDecoder(QoiFormat.Instance, QoiDecoder.Instance);
configuration.ImageFormatsManager.SetEncoder(QoiFormat.Instance, new QoiEncoder());
configuration.ImageFormatsManager.AddImageFormatDetector(new QoiImageFormatDetector());
}
}

27
src/ImageSharp/Formats/Qoi/QoiConstants.cs

@ -0,0 +1,27 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Text;
namespace SixLabors.ImageSharp.Formats.Qoi;
internal static class QoiConstants
{
private static readonly byte[] SMagic = Encoding.UTF8.GetBytes("qoif");
/// <summary>
/// Gets the bytes that indicates the image is QOI
/// </summary>
public static ReadOnlySpan<byte> Magic => SMagic;
/// <summary>
/// Gets the list of mimetypes that equate to a QOI.
/// See https://github.com/phoboslab/qoi/issues/167
/// </summary>
public static string[] MimeTypes { get; } = { "image/qoi", "image/x-qoi", "image/vnd.qoi" };
/// <summary>
/// Gets the list of file extensions that equate to a QOI.
/// </summary>
public static string[] FileExtensions { get; } = { "qoi" };
}

43
src/ImageSharp/Formats/Qoi/QoiDecoder.cs

@ -0,0 +1,43 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Qoi;
internal class QoiDecoder : ImageDecoder
{
private QoiDecoder()
{
}
public static QoiDecoder Instance { get; } = new();
/// <inheritdoc />
protected override Image<TPixel> Decode<TPixel>(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
{
Guard.NotNull(options, nameof(options));
Guard.NotNull(stream, nameof(stream));
QoiDecoderCore decoder = new(options);
Image<TPixel> image = decoder.Decode<TPixel>(options.Configuration, stream, cancellationToken);
ScaleToTargetSize(options, image);
return image;
}
/// <inheritdoc />
protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
{
Guard.NotNull(options, nameof(options));
Guard.NotNull(stream, nameof(stream));
return this.Decode<Rgba32>(options, stream, cancellationToken);
}
protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
{
Guard.NotNull(options, nameof(options));
Guard.NotNull(stream, nameof(stream));
return new QoiDecoderCore(options).Identify(options.Configuration, stream, cancellationToken);
}
}

291
src/ImageSharp/Formats/Qoi/QoiDecoderCore.cs

@ -0,0 +1,291 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Buffers.Binary;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Qoi;
internal class QoiDecoderCore : IImageDecoderInternals
{
/// <summary>
/// The global configuration.
/// </summary>
private readonly Configuration configuration;
/// <summary>
/// Used the manage memory allocations.
/// </summary>
private readonly MemoryAllocator memoryAllocator;
/// <summary>
/// The QOI header.
/// </summary>
private QoiHeader header;
public QoiDecoderCore(DecoderOptions options)
{
this.Options = options;
this.configuration = options.Configuration;
this.memoryAllocator = this.configuration.MemoryAllocator;
}
public DecoderOptions Options { get; }
public Size Dimensions { get; }
/// <inheritdoc />
public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
// Process the header to get metadata
this.ProcessHeader(stream);
// Create Image object
ImageMetadata metadata = new()
{
DecodedImageFormat = QoiFormat.Instance,
HorizontalResolution = this.header.Width,
VerticalResolution = this.header.Height,
ResolutionUnits = PixelResolutionUnit.AspectRatio
};
QoiMetadata qoiMetadata = metadata.GetQoiMetadata();
qoiMetadata.Channels = this.header.Channels;
qoiMetadata.ColorSpace = this.header.ColorSpace;
Image<TPixel> image = new(this.configuration, (int)this.header.Width, (int)this.header.Height, metadata);
Buffer2D<TPixel> pixels = image.GetRootFramePixelBuffer();
this.ProcessPixels(stream, pixels);
return image;
}
/// <inheritdoc />
public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{
this.ProcessHeader(stream);
PixelTypeInfo pixelType = new(8 * (int)this.header.Channels);
Size size = new((int)this.header.Width, (int)this.header.Height);
ImageMetadata metadata = new();
QoiMetadata qoiMetadata = metadata.GetQoiMetadata();
qoiMetadata.Channels = this.header.Channels;
qoiMetadata.ColorSpace = this.header.ColorSpace;
return new ImageInfo(pixelType, size, metadata);
}
/// <summary>
/// Processes the 14-byte header to validate the image and save the metadata
/// in <see cref="header"/>
/// </summary>
/// <param name="stream">The stream where the bytes are being read</param>
/// <exception cref="InvalidImageContentException">If the stream doesn't store a qoi image</exception>
private void ProcessHeader(BufferedReadStream stream)
{
Span<byte> magicBytes = stackalloc byte[4];
Span<byte> widthBytes = stackalloc byte[4];
Span<byte> heightBytes = stackalloc byte[4];
// Read magic bytes
int read = stream.Read(magicBytes);
if (read != 4 || !magicBytes.SequenceEqual(QoiConstants.Magic.ToArray()))
{
ThrowInvalidImageContentException();
}
// If it's a qoi image, read the rest of properties
read = stream.Read(widthBytes);
if (read != 4)
{
ThrowInvalidImageContentException();
}
read = stream.Read(heightBytes);
if (read != 4)
{
ThrowInvalidImageContentException();
}
// These numbers are in Big Endian so we have to reverse them to get the real number
uint width = BinaryPrimitives.ReadUInt32BigEndian(widthBytes);
uint height = BinaryPrimitives.ReadUInt32BigEndian(heightBytes);
if (width == 0 || height == 0)
{
throw new InvalidImageContentException(
$"The image has an invalid size: width = {width}, height = {height}");
}
int channels = stream.ReadByte();
if (channels is -1 or (not 3 and not 4))
{
ThrowInvalidImageContentException();
}
int colorSpace = stream.ReadByte();
if (colorSpace is -1 or (not 0 and not 1))
{
ThrowInvalidImageContentException();
}
this.header = new QoiHeader(width, height, (QoiChannels)channels, (QoiColorSpace)colorSpace);
}
[DoesNotReturn]
private static void ThrowInvalidImageContentException()
=> throw new InvalidImageContentException("The image is not a valid QOI image.");
private void ProcessPixels<TPixel>(BufferedReadStream stream, Buffer2D<TPixel> pixels)
where TPixel : unmanaged, IPixel<TPixel>
{
using IMemoryOwner<Rgba32> previouslySeenPixelsBuffer = this.memoryAllocator.Allocate<Rgba32>(64, AllocationOptions.Clean);
Span<Rgba32> previouslySeenPixels = previouslySeenPixelsBuffer.GetSpan();
Rgba32 previousPixel = new(0, 0, 0, 255);
// We save the pixel to avoid loosing the fully opaque black pixel
// See https://github.com/phoboslab/qoi/issues/258
int pixelArrayPosition = GetArrayPosition(previousPixel);
previouslySeenPixels[pixelArrayPosition] = previousPixel;
byte operationByte;
Rgba32 readPixel = default;
Span<byte> pixelBytes = MemoryMarshal.CreateSpan(ref Unsafe.As<Rgba32, byte>(ref readPixel), 4);
TPixel pixel = default;
for (int i = 0; i < this.header.Height; i++)
{
Span<TPixel> row = pixels.DangerousGetRowSpan(i);
for (int j = 0; j < row.Length; j++)
{
operationByte = (byte)stream.ReadByte();
switch ((QoiChunk)operationByte)
{
// Reading one pixel with previous alpha intact
case QoiChunk.QoiOpRgb:
if (stream.Read(pixelBytes[..3]) < 3)
{
ThrowInvalidImageContentException();
}
readPixel.A = previousPixel.A;
pixel.FromRgba32(readPixel);
pixelArrayPosition = GetArrayPosition(readPixel);
previouslySeenPixels[pixelArrayPosition] = readPixel;
break;
// Reading one pixel with new alpha
case QoiChunk.QoiOpRgba:
if (stream.Read(pixelBytes) < 4)
{
ThrowInvalidImageContentException();
}
pixel.FromRgba32(readPixel);
pixelArrayPosition = GetArrayPosition(readPixel);
previouslySeenPixels[pixelArrayPosition] = readPixel;
break;
default:
switch ((QoiChunk)(operationByte & 0b11000000))
{
// Getting one pixel from previously seen pixels
case QoiChunk.QoiOpIndex:
readPixel = previouslySeenPixels[operationByte];
pixel.FromRgba32(readPixel);
break;
// Get one pixel from the difference (-2..1) of the previous pixel
case QoiChunk.QoiOpDiff:
int redDifference = (operationByte & 0b00110000) >> 4;
int greenDifference = (operationByte & 0b00001100) >> 2;
int blueDifference = operationByte & 0b00000011;
readPixel = previousPixel with
{
R = (byte)Numerics.Modulo256(previousPixel.R + (redDifference - 2)),
G = (byte)Numerics.Modulo256(previousPixel.G + (greenDifference - 2)),
B = (byte)Numerics.Modulo256(previousPixel.B + (blueDifference - 2))
};
pixel.FromRgba32(readPixel);
pixelArrayPosition = GetArrayPosition(readPixel);
previouslySeenPixels[pixelArrayPosition] = readPixel;
break;
// Get green difference in 6 bits and red and blue differences
// depending on the green one
case QoiChunk.QoiOpLuma:
int diffGreen = operationByte & 0b00111111;
int currentGreen = Numerics.Modulo256(previousPixel.G + (diffGreen - 32));
int nextByte = stream.ReadByte();
int diffRedDG = nextByte >> 4;
int diffBlueDG = nextByte & 0b00001111;
int currentRed = Numerics.Modulo256(diffRedDG - 8 + (diffGreen - 32) + previousPixel.R);
int currentBlue = Numerics.Modulo256(diffBlueDG - 8 + (diffGreen - 32) + previousPixel.B);
readPixel = previousPixel with { R = (byte)currentRed, B = (byte)currentBlue, G = (byte)currentGreen };
pixel.FromRgba32(readPixel);
pixelArrayPosition = GetArrayPosition(readPixel);
previouslySeenPixels[pixelArrayPosition] = readPixel;
break;
// Repeating the previous pixel 1..63 times
case QoiChunk.QoiOpRun:
int repetitions = operationByte & 0b00111111;
if (repetitions is 62 or 63)
{
ThrowInvalidImageContentException();
}
readPixel = previousPixel;
pixel.FromRgba32(readPixel);
for (int k = -1; k < repetitions; k++, j++)
{
if (j == row.Length)
{
j = 0;
i++;
row = pixels.DangerousGetRowSpan(i);
}
row[j] = pixel;
}
j--;
continue;
default:
ThrowInvalidImageContentException();
return;
}
break;
}
row[j] = pixel;
previousPixel = readPixel;
}
}
// Check stream end
for (int i = 0; i < 7; i++)
{
if (stream.ReadByte() != 0)
{
ThrowInvalidImageContentException();
}
}
if (stream.ReadByte() != 1)
{
ThrowInvalidImageContentException();
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int GetArrayPosition(Rgba32 pixel)
=> Numerics.Modulo64((pixel.R * 3) + (pixel.G * 5) + (pixel.B * 7) + (pixel.A * 11));
}

33
src/ImageSharp/Formats/Qoi/QoiEncoder.cs

@ -0,0 +1,33 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Advanced;
namespace SixLabors.ImageSharp.Formats.Qoi;
/// <summary>
/// Image encoder for writing an image to a stream as a QOI image
/// </summary>
public class QoiEncoder : ImageEncoder
{
/// <summary>
/// Gets the color channels on the image that can be
/// RGB or RGBA. This is purely informative. It doesn't
/// change the way data chunks are encoded.
/// </summary>
public QoiChannels? Channels { get; init; }
/// <summary>
/// Gets the color space of the image that can be sRGB with
/// linear alpha or all channels linear. This is purely
/// informative. It doesn't change the way data chunks are encoded.
/// </summary>
public QoiColorSpace? ColorSpace { get; init; }
/// <inheritdoc />
protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{
QoiEncoderCore encoder = new(this, image.GetMemoryAllocator(), image.GetConfiguration());
encoder.Encode(image, stream, cancellationToken);
}
}

231
src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs

@ -0,0 +1,231 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Buffers.Binary;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Qoi;
/// <summary>
/// Image encoder for writing an image to a stream as a QOi image
/// </summary>
internal class QoiEncoderCore : IImageEncoderInternals
{
/// <summary>
/// The encoder with options
/// </summary>
private readonly QoiEncoder encoder;
/// <summary>
/// Used the manage memory allocations.
/// </summary>
private readonly MemoryAllocator memoryAllocator;
/// <summary>
/// The configuration instance for the encoding operation.
/// </summary>
private readonly Configuration configuration;
/// <summary>
/// Initializes a new instance of the <see cref="QoiEncoderCore"/> class.
/// </summary>
/// <param name="encoder">The encoder with options.</param>
/// <param name="memoryAllocator">The <see cref="MemoryAllocator" /> to use for buffer allocations.</param>
/// <param name="configuration">The configuration of the Encoder.</param>
public QoiEncoderCore(QoiEncoder encoder, MemoryAllocator memoryAllocator, Configuration configuration)
{
this.encoder = encoder;
this.memoryAllocator = memoryAllocator;
this.configuration = configuration;
}
/// <inheritdoc />
public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
Guard.NotNull(image, nameof(image));
Guard.NotNull(stream, nameof(stream));
this.WriteHeader(image, stream);
this.WritePixels(image, stream);
WriteEndOfStream(stream);
stream.Flush();
}
private void WriteHeader(Image image, Stream stream)
{
// Get metadata
Span<byte> width = stackalloc byte[4];
Span<byte> height = stackalloc byte[4];
BinaryPrimitives.WriteUInt32BigEndian(width, (uint)image.Width);
BinaryPrimitives.WriteUInt32BigEndian(height, (uint)image.Height);
QoiChannels qoiChannels = this.encoder.Channels ?? QoiChannels.Rgba;
QoiColorSpace qoiColorSpace = this.encoder.ColorSpace ?? QoiColorSpace.SrgbWithLinearAlpha;
// Write header to the stream
stream.Write(QoiConstants.Magic);
stream.Write(width);
stream.Write(height);
stream.WriteByte((byte)qoiChannels);
stream.WriteByte((byte)qoiColorSpace);
}
private void WritePixels<TPixel>(Image<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel>
{
// Start image encoding
using IMemoryOwner<Rgba32> previouslySeenPixelsBuffer = this.memoryAllocator.Allocate<Rgba32>(64, AllocationOptions.Clean);
Span<Rgba32> previouslySeenPixels = previouslySeenPixelsBuffer.GetSpan();
Rgba32 previousPixel = new(0, 0, 0, 255);
Rgba32 currentRgba32 = default;
Buffer2D<TPixel> pixels = image.Frames[0].PixelBuffer;
using IMemoryOwner<Rgba32> rgbaRowBuffer = this.memoryAllocator.Allocate<Rgba32>(pixels.Width);
Span<Rgba32> rgbaRow = rgbaRowBuffer.GetSpan();
for (int i = 0; i < pixels.Height; i++)
{
Span<TPixel> row = pixels.DangerousGetRowSpan(i);
PixelOperations<TPixel>.Instance.ToRgba32(this.configuration, row, rgbaRow);
for (int j = 0; j < row.Length && i < pixels.Height; j++)
{
// We get the RGBA value from pixels
currentRgba32 = rgbaRow[j];
// First, we check if the current pixel is equal to the previous one
// If so, we do a QOI_OP_RUN
if (currentRgba32.Equals(previousPixel))
{
/* It looks like this isn't an error, but this makes possible that
* files start with a QOI_OP_RUN if their first pixel is a fully opaque
* black. However, the decoder of this project takes that into consideration
*
* To further details, see https://github.com/phoboslab/qoi/issues/258,
* and we should discuss what to do about this approach and
* if it's correct
*/
int repetitions = 0;
do
{
repetitions++;
j++;
if (j == row.Length)
{
j = 0;
i++;
if (i == pixels.Height)
{
break;
}
row = pixels.DangerousGetRowSpan(i);
PixelOperations<TPixel>.Instance.ToRgba32(this.configuration, row, rgbaRow);
}
currentRgba32 = rgbaRow[j];
}
while (currentRgba32.Equals(previousPixel) && repetitions < 62);
j--;
stream.WriteByte((byte)((int)QoiChunk.QoiOpRun | (repetitions - 1)));
/* If it's a QOI_OP_RUN, we don't overwrite the previous pixel since
* it will be taken and compared on the next iteration
*/
continue;
}
// else, we check if it exists in the previously seen pixels
// If so, we do a QOI_OP_INDEX
int pixelArrayPosition = GetArrayPosition(currentRgba32);
if (previouslySeenPixels[pixelArrayPosition].Equals(currentRgba32))
{
stream.WriteByte((byte)pixelArrayPosition);
}
else
{
// else, we check if the difference is less than -2..1
// Since it wasn't found on the previously seen pixels, we save it
previouslySeenPixels[pixelArrayPosition] = currentRgba32;
int diffRed = currentRgba32.R - previousPixel.R;
int diffGreen = currentRgba32.G - previousPixel.G;
int diffBlue = currentRgba32.B - previousPixel.B;
// If so, we do a QOI_OP_DIFF
if (diffRed is >= -2 and <= 1 &&
diffGreen is >= -2 and <= 1 &&
diffBlue is >= -2 and <= 1 &&
currentRgba32.A == previousPixel.A)
{
// Bottom limit is -2, so we add 2 to make it equal to 0
int dr = diffRed + 2;
int dg = diffGreen + 2;
int db = diffBlue + 2;
byte valueToWrite = (byte)((int)QoiChunk.QoiOpDiff | (dr << 4) | (dg << 2) | db);
stream.WriteByte(valueToWrite);
}
else
{
// else, we check if the green difference is less than -32..31 and the rest -8..7
// If so, we do a QOI_OP_LUMA
int diffRedGreen = diffRed - diffGreen;
int diffBlueGreen = diffBlue - diffGreen;
if (diffGreen is >= -32 and <= 31 &&
diffRedGreen is >= -8 and <= 7 &&
diffBlueGreen is >= -8 and <= 7 &&
currentRgba32.A == previousPixel.A)
{
int dr_dg = diffRedGreen + 8;
int db_dg = diffBlueGreen + 8;
byte byteToWrite1 = (byte)((int)QoiChunk.QoiOpLuma | (diffGreen + 32));
byte byteToWrite2 = (byte)((dr_dg << 4) | db_dg);
stream.WriteByte(byteToWrite1);
stream.WriteByte(byteToWrite2);
}
else
{
// else, we check if the alpha is equal to the previous pixel
// If so, we do a QOI_OP_RGB
if (currentRgba32.A == previousPixel.A)
{
stream.WriteByte((byte)QoiChunk.QoiOpRgb);
stream.WriteByte(currentRgba32.R);
stream.WriteByte(currentRgba32.G);
stream.WriteByte(currentRgba32.B);
}
else
{
// else, we do a QOI_OP_RGBA
stream.WriteByte((byte)QoiChunk.QoiOpRgba);
stream.WriteByte(currentRgba32.R);
stream.WriteByte(currentRgba32.G);
stream.WriteByte(currentRgba32.B);
stream.WriteByte(currentRgba32.A);
}
}
}
}
previousPixel = currentRgba32;
}
}
}
private static void WriteEndOfStream(Stream stream)
{
// Write bytes to end stream
for (int i = 0; i < 7; i++)
{
stream.WriteByte(0);
}
stream.WriteByte(1);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int GetArrayPosition(Rgba32 pixel)
=> Numerics.Modulo64((pixel.R * 3) + (pixel.G * 5) + (pixel.B * 7) + (pixel.A * 11));
}

34
src/ImageSharp/Formats/Qoi/QoiFormat.cs

@ -0,0 +1,34 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Qoi;
/// <summary>
/// Registers the image encoders, decoders and mime type detectors for the qoi format.
/// </summary>
public sealed class QoiFormat : IImageFormat<QoiMetadata>
{
private QoiFormat()
{
}
/// <summary>
/// Gets the shared instance.
/// </summary>
public static QoiFormat Instance { get; } = new QoiFormat();
/// <inheritdoc/>
public string DefaultMimeType => "image/qoi";
/// <inheritdoc/>
public string Name => "QOI";
/// <inheritdoc/>
public IEnumerable<string> MimeTypes => QoiConstants.MimeTypes;
/// <inheritdoc/>
public IEnumerable<string> FileExtensions => QoiConstants.FileExtensions;
/// <inheritdoc/>
public QoiMetadata CreateDefaultFormatMetadata() => new();
}

45
src/ImageSharp/Formats/Qoi/QoiHeader.cs

@ -0,0 +1,45 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Text;
namespace SixLabors.ImageSharp.Formats.Qoi;
/// <summary>
/// Represents the qoi header chunk.
/// </summary>
internal readonly struct QoiHeader
{
public QoiHeader(uint width, uint height, QoiChannels channels, QoiColorSpace colorSpace)
{
this.Width = width;
this.Height = height;
this.Channels = channels;
this.ColorSpace = colorSpace;
}
/// <summary>
/// Gets the magic bytes "qoif"
/// </summary>
public byte[] Magic { get; } = Encoding.UTF8.GetBytes("qoif");
/// <summary>
/// Gets the image width in pixels (Big Endian)
/// </summary>
public uint Width { get; }
/// <summary>
/// Gets the image height in pixels (Big Endian)
/// </summary>
public uint Height { get; }
/// <summary>
/// Gets the color channels of the image. 3 = RGB, 4 = RGBA.
/// </summary>
public QoiChannels Channels { get; }
/// <summary>
/// Gets the color space of the image. 0 = sRGB with linear alpha, 1 = All channels linear
/// </summary>
public QoiColorSpace ColorSpace { get; }
}

25
src/ImageSharp/Formats/Qoi/QoiImageFormatDetector.cs

@ -0,0 +1,25 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics.CodeAnalysis;
namespace SixLabors.ImageSharp.Formats.Qoi;
/// <summary>
/// Detects qoi file headers
/// </summary>
public class QoiImageFormatDetector : IImageFormatDetector
{
/// <inheritdoc/>
public int HeaderSize => 14;
/// <inheritdoc/>
public bool TryDetectFormat(ReadOnlySpan<byte> header, [NotNullWhen(true)] out IImageFormat? format)
{
format = this.IsSupportedFileFormat(header) ? QoiFormat.Instance : null;
return format != null;
}
private bool IsSupportedFileFormat(ReadOnlySpan<byte> header)
=> header.Length >= this.HeaderSize && QoiConstants.Magic.SequenceEqual(header[..4]);
}

40
src/ImageSharp/Formats/Qoi/QoiMetadata.cs

@ -0,0 +1,40 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Qoi;
/// <summary>
/// Provides Qoi specific metadata information for the image.
/// </summary>
public class QoiMetadata : IDeepCloneable
{
/// <summary>
/// Initializes a new instance of the <see cref="QoiMetadata"/> class.
/// </summary>
public QoiMetadata()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="QoiMetadata"/> class.
/// </summary>
/// <param name="other">The metadata to create an instance from.</param>
private QoiMetadata(QoiMetadata other)
{
this.Channels = other.Channels;
this.ColorSpace = other.ColorSpace;
}
/// <summary>
/// Gets or sets color channels of the image. 3 = RGB, 4 = RGBA.
/// </summary>
public QoiChannels Channels { get; set; }
/// <summary>
/// Gets or sets color space of the image. 0 = sRGB with linear alpha, 1 = All channels linear
/// </summary>
public QoiColorSpace ColorSpace { get; set; }
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new QoiMetadata(this);
}

BIN
src/ImageSharp/Formats/Qoi/qoi-specification.pdf

Binary file not shown.

20
src/ImageSharp/Formats/Tiff/Compression/Decompressors/T6BitReader.cs

@ -125,13 +125,29 @@ internal sealed class T6BitReader : T4BitReader
if (value == Len7Code0000000.Code)
{
this.Code = Len7Code0000000;
return false;
// We do not support Extensions1D codes, but some encoders (scanner from epson) write a premature EOL code,
// which at this point cannot be distinguished from the marker, because we read the data bit by bit.
// Read the next 5 bit, if its a EOL code return true, indicating its the end of the image.
if (this.ReadValue(5) == 1)
{
return true;
}
throw new NotSupportedException("ccitt extensions 1D codes are not supported.");
}
if (value == Len7Code0000001.Code)
{
this.Code = Len7Code0000001;
return false;
// Same as above, we do not support Extensions2D codes, but it could be a EOL instead.
if (this.ReadValue(5) == 1)
{
return true;
}
throw new NotSupportedException("ccitt extensions 2D codes are not supported.");
}
if (value == Len7Code0000011.Code)

1
src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs

@ -44,6 +44,7 @@ internal static class HorizontalPredictor
UndoRgb24Bit(pixelBytes, width);
break;
case TiffColorType.Rgba8888:
case TiffColorType.Cmyk:
UndoRgba32Bit(pixelBytes, width);
break;
case TiffColorType.Rgb161616:

2
src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs

@ -344,7 +344,7 @@ internal class TiffDecoderCore : IImageDecoderInternals
ArgumentNullException.ThrowIfNull(valueWidth);
}
if (!tags.TryGetValue(ExifTag.TileWidth, out IExifValue<Number> valueLength))
if (!tags.TryGetValue(ExifTag.TileLength, out IExifValue<Number> valueLength))
{
ArgumentNullException.ThrowIfNull(valueLength);
}

24
src/ImageSharp/Formats/Webp/Lossy/Vp8Residual.cs

@ -156,7 +156,7 @@ internal class Vp8Residual
return LossyUtils.Vp8BitCost(0, (byte)p0);
}
if (Avx2.IsSupported)
if (Sse2.IsSupported)
{
Span<byte> scratch = stackalloc byte[32];
Span<byte> ctxs = scratch.Slice(0, 16);
@ -165,19 +165,23 @@ internal class Vp8Residual
// Precompute clamped levels and contexts, packed to 8b.
ref short outputRef = ref MemoryMarshal.GetReference<short>(this.Coeffs);
Vector256<short> c0 = Unsafe.As<short, Vector256<byte>>(ref outputRef).AsInt16();
Vector256<short> d0 = Avx2.Subtract(Vector256<short>.Zero, c0);
Vector256<short> e0 = Avx2.Max(c0, d0); // abs(v), 16b
Vector256<sbyte> f = Avx2.PackSignedSaturate(e0, e0);
Vector256<byte> g = Avx2.Min(f.AsByte(), Vector256.Create((byte)2));
Vector256<byte> h = Avx2.Min(f.AsByte(), Vector256.Create((byte)67)); // clampLevel in [0..67]
Vector128<short> c0 = Unsafe.As<short, Vector128<byte>>(ref outputRef).AsInt16();
Vector128<short> c1 = Unsafe.As<short, Vector128<byte>>(ref Unsafe.Add(ref outputRef, 8)).AsInt16();
Vector128<short> d0 = Sse2.Subtract(Vector128<short>.Zero, c0);
Vector128<short> d1 = Sse2.Subtract(Vector128<short>.Zero, c1);
Vector128<short> e0 = Sse2.Max(c0, d0); // abs(v), 16b
Vector128<short> e1 = Sse2.Max(c1, d1);
Vector128<sbyte> f = Sse2.PackSignedSaturate(e0, e1);
Vector128<byte> g = Sse2.Min(f.AsByte(), Vector128.Create((byte)2)); // context = 0, 1, 2
Vector128<byte> h = Sse2.Min(f.AsByte(), Vector128.Create((byte)67)); // clampLevel in [0..67]
ref byte ctxsRef = ref MemoryMarshal.GetReference(ctxs);
ref byte levelsRef = ref MemoryMarshal.GetReference(levels);
ref ushort absLevelsRef = ref MemoryMarshal.GetReference(absLevels);
Unsafe.As<byte, Vector128<byte>>(ref ctxsRef) = g.GetLower();
Unsafe.As<byte, Vector128<byte>>(ref levelsRef) = h.GetLower();
Unsafe.As<ushort, Vector256<ushort>>(ref absLevelsRef) = e0.AsUInt16();
Unsafe.As<byte, Vector128<byte>>(ref ctxsRef) = g;
Unsafe.As<byte, Vector128<byte>>(ref levelsRef) = h;
Unsafe.As<ushort, Vector128<ushort>>(ref absLevelsRef) = e0.AsUInt16();
Unsafe.As<ushort, Vector128<ushort>>(ref Unsafe.Add(ref absLevelsRef, 8)) = e1.AsUInt16();
int level;
int flevel;

26
src/ImageSharp/IO/IFileSystem.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.IO;
@ -9,16 +9,32 @@ namespace SixLabors.ImageSharp.IO;
internal interface IFileSystem
{
/// <summary>
/// Returns a readable stream as defined by the path.
/// Opens a file as defined by the path and returns it as a readable stream.
/// </summary>
/// <param name="path">Path to the file to open.</param>
/// <returns>A stream representing the file to open.</returns>
/// <returns>A stream representing the opened file.</returns>
Stream OpenRead(string path);
/// <summary>
/// Creates or opens a file and returns it as a writable stream as defined by the path.
/// Opens a file as defined by the path and returns it as a readable stream
/// that can be used for asynchronous reading.
/// </summary>
/// <param name="path">Path to the file to open.</param>
/// <returns>A stream representing the file to open.</returns>
/// <returns>A stream representing the opened file.</returns>
Stream OpenReadAsynchronous(string path);
/// <summary>
/// Creates or opens a file as defined by the path and returns it as a writable stream.
/// </summary>
/// <param name="path">Path to the file to open.</param>
/// <returns>A stream representing the opened file.</returns>
Stream Create(string path);
/// <summary>
/// Creates or opens a file as defined by the path and returns it as a writable stream
/// that can be used for asynchronous reading and writing.
/// </summary>
/// <param name="path">Path to the file to open.</param>
/// <returns>A stream representing the opened file.</returns>
Stream CreateAsynchronous(string path);
}

20
src/ImageSharp/IO/LocalFileSystem.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.IO;
@ -11,6 +11,24 @@ internal sealed class LocalFileSystem : IFileSystem
/// <inheritdoc/>
public Stream OpenRead(string path) => File.OpenRead(path);
/// <inheritdoc/>
public Stream OpenReadAsynchronous(string path) => File.Open(path, new FileStreamOptions
{
Mode = FileMode.Open,
Access = FileAccess.Read,
Share = FileShare.Read,
Options = FileOptions.Asynchronous,
});
/// <inheritdoc/>
public Stream Create(string path) => File.Create(path);
/// <inheritdoc/>
public Stream CreateAsynchronous(string path) => File.Open(path, new FileStreamOptions
{
Mode = FileMode.Create,
Access = FileAccess.ReadWrite,
Share = FileShare.None,
Options = FileOptions.Asynchronous,
});
}

10
src/ImageSharp/Image.FromFile.cs

@ -72,7 +72,7 @@ public abstract partial class Image
{
Guard.NotNull(options, nameof(options));
using Stream stream = options.Configuration.FileSystem.OpenRead(path);
await using Stream stream = options.Configuration.FileSystem.OpenReadAsynchronous(path);
return await DetectFormatAsync(options, stream, cancellationToken).ConfigureAwait(false);
}
@ -81,7 +81,7 @@ public abstract partial class Image
/// A return value indicates whether the operation succeeded.
/// </summary>
/// <param name="path">The image file to open and to read the header from.</param>
/// <returns><see langword="true"/> if the information can be read; otherwise, <see langword="false"/></returns>
/// <returns>The <see cref="ImageInfo"/>.</returns>
/// <exception cref="ArgumentNullException">The path is null.</exception>
/// <exception cref="NotSupportedException">The file stream is not readable or the image format is not supported.</exception>
/// <exception cref="InvalidImageContentException">The encoded image contains invalid content.</exception>
@ -144,7 +144,7 @@ public abstract partial class Image
CancellationToken cancellationToken = default)
{
Guard.NotNull(options, nameof(options));
using Stream stream = options.Configuration.FileSystem.OpenRead(path);
await using Stream stream = options.Configuration.FileSystem.OpenReadAsynchronous(path);
return await IdentifyAsync(options, stream, cancellationToken).ConfigureAwait(false);
}
@ -214,7 +214,7 @@ public abstract partial class Image
string path,
CancellationToken cancellationToken = default)
{
using Stream stream = options.Configuration.FileSystem.OpenRead(path);
await using Stream stream = options.Configuration.FileSystem.OpenReadAsynchronous(path);
return await LoadAsync(options, stream, cancellationToken).ConfigureAwait(false);
}
@ -291,7 +291,7 @@ public abstract partial class Image
Guard.NotNull(options, nameof(options));
Guard.NotNull(path, nameof(path));
using Stream stream = options.Configuration.FileSystem.OpenRead(path);
await using Stream stream = options.Configuration.FileSystem.OpenReadAsynchronous(path);
return await LoadAsync<TPixel>(options, stream, cancellationToken).ConfigureAwait(false);
}
}

4
src/ImageSharp/Image.FromStream.cs

@ -29,7 +29,7 @@ public abstract partial class Image
/// </summary>
/// <param name="options">The general decoder options.</param>
/// <param name="stream">The image stream to read the header from.</param>
/// <returns><see langword="true"/> if a match is found; otherwise, <see langword="false"/></returns>
/// <returns>The <see cref="IImageFormat"/>.</returns>
/// <exception cref="ArgumentNullException">The options are null.</exception>
/// <exception cref="ArgumentNullException">The stream is null.</exception>
/// <exception cref="NotSupportedException">The stream is not readable or the image format is not supported.</exception>
@ -79,7 +79,7 @@ public abstract partial class Image
/// Reads the raw image information from the specified stream without fully decoding it.
/// </summary>
/// <param name="stream">The image stream to read the header from.</param>
/// <returns><see langword="true"/> if the information can be read; otherwise, <see langword="false"/></returns>
/// <returns>The <see cref="ImageInfo"/>.</returns>
/// <exception cref="ArgumentNullException">The stream is null.</exception>
/// <exception cref="NotSupportedException">The stream is not readable or the image format is not supported.</exception>
/// <exception cref="InvalidImageContentException">The encoded image contains invalid content.</exception>

2
src/ImageSharp/ImageExtensions.cs

@ -70,7 +70,7 @@ public static partial class ImageExtensions
Guard.NotNull(path, nameof(path));
Guard.NotNull(encoder, nameof(encoder));
using Stream fs = source.GetConfiguration().FileSystem.Create(path);
await using Stream fs = source.GetConfiguration().FileSystem.CreateAsynchronous(path);
await source.SaveAsync(fs, encoder, cancellationToken).ConfigureAwait(false);
}

7
src/ImageSharp/Image{TPixel}.cs

@ -418,12 +418,7 @@ public sealed class Image<TPixel> : Image
{
Guard.NotNull(frames, nameof(frames));
ImageFrame<TPixel>? rootFrame = frames.FirstOrDefault();
if (rootFrame == null)
{
throw new ArgumentException("Must not be empty.", nameof(frames));
}
ImageFrame<TPixel>? rootFrame = frames.FirstOrDefault() ?? throw new ArgumentException("Must not be empty.", nameof(frames));
Size rootSize = rootFrame.Size();

2
src/ImageSharp/PixelFormats/PixelImplementations/Rgb48.cs

@ -8,7 +8,7 @@ using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.PixelFormats;
/// <summary>
/// Packed pixel type containing three 16-bit unsigned normalized values ranging from 0 to 635535.
/// Packed pixel type containing three 16-bit unsigned normalized values ranging from 0 to 65535.
/// <para>
/// Ranges from [0, 0, 0, 1] to [1, 1, 1, 1] in vector form.
/// </para>

10
src/ImageSharp/Primitives/LongRational.cs

@ -131,6 +131,11 @@ internal readonly struct LongRational : IEquatable<LongRational>
/// <param name="bestPrecision">Whether to use the best possible precision when parsing the value.</param>
public static LongRational FromDouble(double value, bool bestPrecision)
{
if (value == 0.0)
{
return new LongRational(0, 1);
}
if (double.IsNaN(value))
{
return new LongRational(0, 0);
@ -201,11 +206,6 @@ internal readonly struct LongRational : IEquatable<LongRational>
return this;
}
if (this.Numerator == 0)
{
return new LongRational(0, 0);
}
if (this.Numerator == this.Denominator)
{
return new LongRational(1, 1);

162
src/ImageSharp/Processing/Extensions/Drawing/DrawImageExtensions.cs

@ -15,277 +15,277 @@ public static class DrawImageExtensions
/// Draws the given image together with the currently processing image by blending their pixels.
/// </summary>
/// <param name="source">The current image processing context.</param>
/// <param name="image">The image to draw on the currently processing image.</param>
/// <param name="foreground">The image to draw on the currently processing image.</param>
/// <param name="opacity">The opacity of the image to draw. Must be between 0 and 1.</param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext DrawImage(
this IImageProcessingContext source,
Image image,
Image foreground,
float opacity)
{
GraphicsOptions options = source.GetGraphicsOptions();
return DrawImage(source, image, options.ColorBlendingMode, options.AlphaCompositionMode, opacity);
return DrawImage(source, foreground, options.ColorBlendingMode, options.AlphaCompositionMode, opacity);
}
/// <summary>
/// Draws the given image together with the currently processing image by blending their pixels.
/// </summary>
/// <param name="source">The current image processing context.</param>
/// <param name="image">The image to draw on the currently processing image.</param>
/// <param name="rectangle">The rectangle structure that specifies the portion of the image to draw.</param>
/// <param name="foreground">The image to draw on the currently processing image.</param>
/// <param name="foregroundRectangle">The rectangle structure that specifies the portion of the image to draw.</param>
/// <param name="opacity">The opacity of the image to draw. Must be between 0 and 1.</param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext DrawImage(
this IImageProcessingContext source,
Image image,
Rectangle rectangle,
Image foreground,
Rectangle foregroundRectangle,
float opacity)
{
GraphicsOptions options = source.GetGraphicsOptions();
return DrawImage(source, image, rectangle, options.ColorBlendingMode, options.AlphaCompositionMode, opacity);
return DrawImage(source, foreground, foregroundRectangle, options.ColorBlendingMode, options.AlphaCompositionMode, opacity);
}
/// <summary>
/// Draws the given image together with the currently processing image by blending their pixels.
/// </summary>
/// <param name="source">The current image processing context.</param>
/// <param name="image">The image to draw on the currently processing image.</param>
/// <param name="foreground">The image to draw on the currently processing image.</param>
/// <param name="colorBlending">The color blending mode.</param>
/// <param name="opacity">The opacity of the image to draw. Must be between 0 and 1.</param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext DrawImage(
this IImageProcessingContext source,
Image image,
Image foreground,
PixelColorBlendingMode colorBlending,
float opacity)
=> DrawImage(source, image, Point.Empty, colorBlending, opacity);
=> DrawImage(source, foreground, Point.Empty, colorBlending, opacity);
/// <summary>
/// Draws the given image together with the currently processing image by blending their pixels.
/// </summary>
/// <param name="source">The current image processing context.</param>
/// <param name="image">The image to draw on the currently processing image.</param>
/// <param name="rectangle">The rectangle structure that specifies the portion of the image to draw.</param>
/// <param name="foreground">The image to draw on the currently processing image.</param>
/// <param name="foregroundRectangle">The rectangle structure that specifies the portion of the image to draw.</param>
/// <param name="colorBlending">The color blending mode.</param>
/// <param name="opacity">The opacity of the image to draw. Must be between 0 and 1.</param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext DrawImage(
this IImageProcessingContext source,
Image image,
Rectangle rectangle,
Image foreground,
Rectangle foregroundRectangle,
PixelColorBlendingMode colorBlending,
float opacity)
=> DrawImage(source, image, rectangle, colorBlending, source.GetGraphicsOptions().AlphaCompositionMode, opacity);
=> DrawImage(source, foreground, foregroundRectangle, colorBlending, source.GetGraphicsOptions().AlphaCompositionMode, opacity);
/// <summary>
/// Draws the given image together with the currently processing image by blending their pixels.
/// </summary>
/// <param name="source">The current image processing context.</param>
/// <param name="image">The image to draw on the currently processing image.</param>
/// <param name="foreground">The image to draw on the currently processing image.</param>
/// <param name="colorBlending">The color blending mode.</param>
/// <param name="alphaComposition">The alpha composition mode.</param>
/// <param name="opacity">The opacity of the image to draw. Must be between 0 and 1.</param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext DrawImage(
this IImageProcessingContext source,
Image image,
Image foreground,
PixelColorBlendingMode colorBlending,
PixelAlphaCompositionMode alphaComposition,
float opacity)
=> DrawImage(source, image, Point.Empty, colorBlending, alphaComposition, opacity);
=> DrawImage(source, foreground, Point.Empty, colorBlending, alphaComposition, opacity);
/// <summary>
/// Draws the given image together with the currently processing image by blending their pixels.
/// </summary>
/// <param name="source">The current image processing context.</param>
/// <param name="image">The image to draw on the currently processing image.</param>
/// <param name="rectangle">The rectangle structure that specifies the portion of the image to draw.</param>
/// <param name="foreground">The image to draw on the currently processing image.</param>
/// <param name="foregroundRectangle">The rectangle structure that specifies the portion of the image to draw.</param>
/// <param name="colorBlending">The color blending mode.</param>
/// <param name="alphaComposition">The alpha composition mode.</param>
/// <param name="opacity">The opacity of the image to draw. Must be between 0 and 1.</param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext DrawImage(
this IImageProcessingContext source,
Image image,
Rectangle rectangle,
Image foreground,
Rectangle foregroundRectangle,
PixelColorBlendingMode colorBlending,
PixelAlphaCompositionMode alphaComposition,
float opacity)
=> DrawImage(source, image, Point.Empty, rectangle, colorBlending, alphaComposition, opacity);
=> DrawImage(source, foreground, Point.Empty, foregroundRectangle, colorBlending, alphaComposition, opacity);
/// <summary>
/// Draws the given image together with the currently processing image by blending their pixels.
/// </summary>
/// <param name="source">The current image processing context.</param>
/// <param name="image">The image to draw on the currently processing image.</param>
/// <param name="foreground">The image to draw on the currently processing image.</param>
/// <param name="options">The options, including the blending type and blending amount.</param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext DrawImage(
this IImageProcessingContext source,
Image image,
Image foreground,
GraphicsOptions options)
=> DrawImage(source, image, Point.Empty, options);
=> DrawImage(source, foreground, Point.Empty, options);
/// <summary>
/// Draws the given image together with the currently processing image by blending their pixels.
/// </summary>
/// <param name="source">The current image processing context.</param>
/// <param name="image">The image to draw on the currently processing image.</param>
/// <param name="rectangle">The rectangle structure that specifies the portion of the image to draw.</param>
/// <param name="foreground">The image to draw on the currently processing image.</param>
/// <param name="foregroundRectangle">The rectangle structure that specifies the portion of the image to draw.</param>
/// <param name="options">The options, including the blending type and blending amount.</param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext DrawImage(
this IImageProcessingContext source,
Image image,
Rectangle rectangle,
Image foreground,
Rectangle foregroundRectangle,
GraphicsOptions options)
=> DrawImage(source, image, Point.Empty, rectangle, options);
=> DrawImage(source, foreground, Point.Empty, foregroundRectangle, options);
/// <summary>
/// Draws the given image together with the currently processing image by blending their pixels.
/// </summary>
/// <param name="source">The current image processing context.</param>
/// <param name="image">The image to draw on the currently processing image.</param>
/// <param name="location">The location on the currenty processing image at which to draw.</param>
/// <param name="foreground">The image to draw on the currently processing image.</param>
/// <param name="backgroundLocation">The location on the currently processing image at which to draw.</param>
/// <param name="opacity">The opacity of the image to draw. Must be between 0 and 1.</param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext DrawImage(
this IImageProcessingContext source,
Image image,
Point location,
Image foreground,
Point backgroundLocation,
float opacity)
{
GraphicsOptions options = source.GetGraphicsOptions();
return DrawImage(source, image, location, options.ColorBlendingMode, options.AlphaCompositionMode, opacity);
return DrawImage(source, foreground, backgroundLocation, options.ColorBlendingMode, options.AlphaCompositionMode, opacity);
}
/// <summary>
/// Draws the given image together with the currently processing image by blending their pixels.
/// </summary>
/// <param name="source">The current image processing context.</param>
/// <param name="image">The image to draw on the currently processing image.</param>
/// <param name="location">The location on the currenty processing image at which to draw.</param>
/// <param name="rectangle">The rectangle structure that specifies the portion of the image to draw.</param>
/// <param name="foreground">The image to draw on the currently processing image.</param>
/// <param name="backgroundLocation">The location on the currently processing image at which to draw.</param>
/// <param name="foregroundRectangle">The rectangle structure that specifies the portion of the image to draw.</param>
/// <param name="opacity">The opacity of the image to draw. Must be between 0 and 1.</param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext DrawImage(
this IImageProcessingContext source,
Image image,
Point location,
Rectangle rectangle,
Image foreground,
Point backgroundLocation,
Rectangle foregroundRectangle,
float opacity)
{
GraphicsOptions options = source.GetGraphicsOptions();
return DrawImage(source, image, location, rectangle, options.ColorBlendingMode, options.AlphaCompositionMode, opacity);
return DrawImage(source, foreground, backgroundLocation, foregroundRectangle, options.ColorBlendingMode, options.AlphaCompositionMode, opacity);
}
/// <summary>
/// Draws the given image together with the currently processing image by blending their pixels.
/// </summary>
/// <param name="source">The current image processing context.</param>
/// <param name="image">The image to draw on the currently processing image.</param>
/// <param name="location">The location on the currenty processing image at which to draw.</param>
/// <param name="foreground">The image to draw on the currently processing image.</param>
/// <param name="backgroundLocation">The location on the currently processing image at which to draw.</param>
/// <param name="colorBlending">The color blending to apply.</param>
/// <param name="opacity">The opacity of the image to draw. Must be between 0 and 1.</param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext DrawImage(
this IImageProcessingContext source,
Image image,
Point location,
Image foreground,
Point backgroundLocation,
PixelColorBlendingMode colorBlending,
float opacity)
=> DrawImage(source, image, location, colorBlending, source.GetGraphicsOptions().AlphaCompositionMode, opacity);
=> DrawImage(source, foreground, backgroundLocation, colorBlending, source.GetGraphicsOptions().AlphaCompositionMode, opacity);
/// <summary>
/// Draws the given image together with the currently processing image by blending their pixels.
/// </summary>
/// <param name="source">The current image processing context.</param>
/// <param name="image">The image to draw on the currently processing image.</param>
/// <param name="location">The location on the currenty processing image at which to draw.</param>
/// <param name="rectangle">The rectangle structure that specifies the portion of the image to draw.</param>
/// <param name="foreground">The image to draw on the currently processing image.</param>
/// <param name="backgroundLocation">The location on the currently processing image at which to draw.</param>
/// <param name="foregroundRectangle">The rectangle structure that specifies the portion of the image to draw.</param>
/// <param name="colorBlending">The color blending to apply.</param>
/// <param name="opacity">The opacity of the image to draw. Must be between 0 and 1.</param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext DrawImage(
this IImageProcessingContext source,
Image image,
Point location,
Rectangle rectangle,
Image foreground,
Point backgroundLocation,
Rectangle foregroundRectangle,
PixelColorBlendingMode colorBlending,
float opacity)
=> DrawImage(source, image, location, rectangle, colorBlending, source.GetGraphicsOptions().AlphaCompositionMode, opacity);
=> DrawImage(source, foreground, backgroundLocation, foregroundRectangle, colorBlending, source.GetGraphicsOptions().AlphaCompositionMode, opacity);
/// <summary>
/// Draws the given image together with the currently processing image by blending their pixels.
/// </summary>
/// <param name="source">The current image processing context.</param>
/// <param name="image">The image to draw on the currently processing image.</param>
/// <param name="location">The location on the currenty processing image at which to draw.</param>
/// <param name="foreground">The image to draw on the currently processing image.</param>
/// <param name="backgroundLocation">The location on the currently processing image at which to draw.</param>
/// <param name="options">The options containing the blend mode and opacity.</param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext DrawImage(
this IImageProcessingContext source,
Image image,
Point location,
Image foreground,
Point backgroundLocation,
GraphicsOptions options)
=> DrawImage(source, image, location, options.ColorBlendingMode, options.AlphaCompositionMode, options.BlendPercentage);
=> DrawImage(source, foreground, backgroundLocation, options.ColorBlendingMode, options.AlphaCompositionMode, options.BlendPercentage);
/// <summary>
/// Draws the given image together with the currently processing image by blending their pixels.
/// </summary>
/// <param name="source">The current image processing context.</param>
/// <param name="image">The image to draw on the currently processing image.</param>
/// <param name="location">The location on the currenty processing image at which to draw.</param>
/// <param name="rectangle">The rectangle structure that specifies the portion of the image to draw.</param>
/// <param name="foreground">The image to draw on the currently processing image.</param>
/// <param name="backgroundLocation">The location on the currently processing image at which to draw.</param>
/// <param name="foregroundRectangle">The rectangle structure that specifies the portion of the image to draw.</param>
/// <param name="options">The options containing the blend mode and opacity.</param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext DrawImage(
this IImageProcessingContext source,
Image image,
Point location,
Rectangle rectangle,
Image foreground,
Point backgroundLocation,
Rectangle foregroundRectangle,
GraphicsOptions options)
=> DrawImage(source, image, location, rectangle, options.ColorBlendingMode, options.AlphaCompositionMode, options.BlendPercentage);
=> DrawImage(source, foreground, backgroundLocation, foregroundRectangle, options.ColorBlendingMode, options.AlphaCompositionMode, options.BlendPercentage);
/// <summary>
/// Draws the given image together with the currently processing image by blending their pixels.
/// </summary>
/// <param name="source">The current image processing context.</param>
/// <param name="image">The image to draw on the currently processing image.</param>
/// <param name="location">The location on the currenty processing image at which to draw.</param>
/// <param name="foreground">The image to draw on the currently processing image.</param>
/// <param name="backgroundLocation">The location on the currently processing image at which to draw.</param>
/// <param name="colorBlending">The color blending to apply.</param>
/// <param name="alphaComposition">The alpha composition mode.</param>
/// <param name="opacity">The opacity of the image to draw. Must be between 0 and 1.</param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext DrawImage(
this IImageProcessingContext source,
Image image,
Point location,
Image foreground,
Point backgroundLocation,
PixelColorBlendingMode colorBlending,
PixelAlphaCompositionMode alphaComposition,
float opacity)
=> source.ApplyProcessor(new DrawImageProcessor(image, location, colorBlending, alphaComposition, opacity));
=> source.ApplyProcessor(new DrawImageProcessor(foreground, backgroundLocation, foreground.Bounds, colorBlending, alphaComposition, opacity));
/// <summary>
/// Draws the given image together with the currently processing image by blending their pixels.
/// </summary>
/// <param name="source">The current image processing context.</param>
/// <param name="image">The image to draw on the currently processing image.</param>
/// <param name="location">The location on the currenty processing image at which to draw.</param>
/// <param name="rectangle">The rectangle structure that specifies the portion of the image to draw.</param>
/// <param name="foreground">The image to draw on the currently processing image.</param>
/// <param name="backgroundLocation">The location on the currently processing image at which to draw.</param>
/// <param name="foregroundRectangle">The rectangle structure that specifies the portion of the image to draw.</param>
/// <param name="colorBlending">The color blending to apply.</param>
/// <param name="alphaComposition">The alpha composition mode.</param>
/// <param name="opacity">The opacity of the image to draw. Must be between 0 and 1.</param>
/// <returns>The <see cref="IImageProcessingContext"/>.</returns>
public static IImageProcessingContext DrawImage(
this IImageProcessingContext source,
Image image,
Point location,
Rectangle rectangle,
Image foreground,
Point backgroundLocation,
Rectangle foregroundRectangle,
PixelColorBlendingMode colorBlending,
PixelAlphaCompositionMode alphaComposition,
float opacity) =>
source.ApplyProcessor(
new DrawImageProcessor(image, location, colorBlending, alphaComposition, opacity),
rectangle);
new DrawImageProcessor(foreground, backgroundLocation, foregroundRectangle, colorBlending, alphaComposition, opacity),
foregroundRectangle);
}

59
src/ImageSharp/Processing/Processors/Drawing/DrawImageProcessor.cs

@ -14,20 +14,41 @@ public class DrawImageProcessor : IImageProcessor
/// <summary>
/// Initializes a new instance of the <see cref="DrawImageProcessor"/> class.
/// </summary>
/// <param name="image">The image to blend.</param>
/// <param name="location">The location to draw the blended image.</param>
/// <param name="foreground">The image to blend.</param>
/// <param name="backgroundLocation">The location to draw the foreground image on the background.</param>
/// <param name="colorBlendingMode">The blending mode to use when drawing the image.</param>
/// <param name="alphaCompositionMode">The Alpha blending mode to use when drawing the image.</param>
/// <param name="opacity">The opacity of the image to blend.</param>
public DrawImageProcessor(
Image image,
Point location,
Image foreground,
Point backgroundLocation,
PixelColorBlendingMode colorBlendingMode,
PixelAlphaCompositionMode alphaCompositionMode,
float opacity)
: this(foreground, backgroundLocation, foreground.Bounds, colorBlendingMode, alphaCompositionMode, opacity)
{
this.Image = image;
this.Location = location;
}
/// <summary>
/// Initializes a new instance of the <see cref="DrawImageProcessor"/> class.
/// </summary>
/// <param name="foreground">The image to blend.</param>
/// <param name="backgroundLocation">The location to draw the foreground image on the background.</param>
/// <param name="foregroundRectangle">The rectangular portion of the foreground image to draw.</param>
/// <param name="colorBlendingMode">The blending mode to use when drawing the image.</param>
/// <param name="alphaCompositionMode">The Alpha blending mode to use when drawing the image.</param>
/// <param name="opacity">The opacity of the image to blend.</param>
public DrawImageProcessor(
Image foreground,
Point backgroundLocation,
Rectangle foregroundRectangle,
PixelColorBlendingMode colorBlendingMode,
PixelAlphaCompositionMode alphaCompositionMode,
float opacity)
{
this.ForeGround = foreground;
this.BackgroundLocation = backgroundLocation;
this.ForegroundRectangle = foregroundRectangle;
this.ColorBlendingMode = colorBlendingMode;
this.AlphaCompositionMode = alphaCompositionMode;
this.Opacity = opacity;
@ -36,12 +57,17 @@ public class DrawImageProcessor : IImageProcessor
/// <summary>
/// Gets the image to blend.
/// </summary>
public Image Image { get; }
public Image ForeGround { get; }
/// <summary>
/// Gets the location to draw the foreground image on the background.
/// </summary>
public Point BackgroundLocation { get; }
/// <summary>
/// Gets the location to draw the blended image.
/// Gets the rectangular portion of the foreground image to draw.
/// </summary>
public Point Location { get; }
public Rectangle ForegroundRectangle { get; }
/// <summary>
/// Gets the blending mode to use when drawing the image.
@ -62,8 +88,8 @@ public class DrawImageProcessor : IImageProcessor
public IImageProcessor<TPixelBg> CreatePixelSpecificProcessor<TPixelBg>(Configuration configuration, Image<TPixelBg> source, Rectangle sourceRectangle)
where TPixelBg : unmanaged, IPixel<TPixelBg>
{
ProcessorFactoryVisitor<TPixelBg> visitor = new(configuration, this, source, sourceRectangle);
this.Image.AcceptVisitor(visitor);
ProcessorFactoryVisitor<TPixelBg> visitor = new(configuration, this, source);
this.ForeGround.AcceptVisitor(visitor);
return visitor.Result!;
}
@ -73,14 +99,15 @@ public class DrawImageProcessor : IImageProcessor
private readonly Configuration configuration;
private readonly DrawImageProcessor definition;
private readonly Image<TPixelBg> source;
private readonly Rectangle sourceRectangle;
public ProcessorFactoryVisitor(Configuration configuration, DrawImageProcessor definition, Image<TPixelBg> source, Rectangle sourceRectangle)
public ProcessorFactoryVisitor(
Configuration configuration,
DrawImageProcessor definition,
Image<TPixelBg> source)
{
this.configuration = configuration;
this.definition = definition;
this.source = source;
this.sourceRectangle = sourceRectangle;
}
public IImageProcessor<TPixelBg>? Result { get; private set; }
@ -91,8 +118,8 @@ public class DrawImageProcessor : IImageProcessor
this.configuration,
image,
this.source,
this.sourceRectangle,
this.definition.Location,
this.definition.BackgroundLocation,
this.definition.ForegroundRectangle,
this.definition.ColorBlendingMode,
this.definition.AlphaCompositionMode,
this.definition.Opacity);

132
src/ImageSharp/Processing/Processors/Drawing/DrawImageProcessor{TPixelBg,TPixelFg}.cs

@ -21,36 +21,42 @@ internal class DrawImageProcessor<TPixelBg, TPixelFg> : ImageProcessor<TPixelBg>
/// Initializes a new instance of the <see cref="DrawImageProcessor{TPixelBg, TPixelFg}"/> class.
/// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <param name="image">The foreground <see cref="Image{TPixelFg}"/> to blend with the currently processing image.</param>
/// <param name="source">The source <see cref="Image{TPixelBg}"/> for the current processor instance.</param>
/// <param name="sourceRectangle">The source area to process for the current processor instance.</param>
/// <param name="location">The location to draw the blended image.</param>
/// <param name="foregroundImage">The foreground <see cref="Image{TPixelFg}"/> to blend with the currently processing image.</param>
/// <param name="backgroundImage">The source <see cref="Image{TPixelBg}"/> for the current processor instance.</param>
/// <param name="backgroundLocation">The location to draw the blended image.</param>
/// <param name="foregroundRectangle">The source area to process for the current processor instance.</param>
/// <param name="colorBlendingMode">The blending mode to use when drawing the image.</param>
/// <param name="alphaCompositionMode">The Alpha blending mode to use when drawing the image.</param>
/// <param name="alphaCompositionMode">The alpha blending mode to use when drawing the image.</param>
/// <param name="opacity">The opacity of the image to blend. Must be between 0 and 1.</param>
public DrawImageProcessor(
Configuration configuration,
Image<TPixelFg> image,
Image<TPixelBg> source,
Rectangle sourceRectangle,
Point location,
Image<TPixelFg> foregroundImage,
Image<TPixelBg> backgroundImage,
Point backgroundLocation,
Rectangle foregroundRectangle,
PixelColorBlendingMode colorBlendingMode,
PixelAlphaCompositionMode alphaCompositionMode,
float opacity)
: base(configuration, source, sourceRectangle)
: base(configuration, backgroundImage, backgroundImage.Bounds)
{
Guard.MustBeBetweenOrEqualTo(opacity, 0, 1, nameof(opacity));
this.Image = image;
this.ForegroundImage = foregroundImage;
this.ForegroundRectangle = foregroundRectangle;
this.Opacity = opacity;
this.Blender = PixelOperations<TPixelBg>.Instance.GetPixelBlender(colorBlendingMode, alphaCompositionMode);
this.Location = location;
this.BackgroundLocation = backgroundLocation;
}
/// <summary>
/// Gets the image to blend
/// </summary>
public Image<TPixelFg> Image { get; }
public Image<TPixelFg> ForegroundImage { get; }
/// <summary>
/// Gets the rectangular portion of the foreground image to draw.
/// </summary>
public Rectangle ForegroundRectangle { get; }
/// <summary>
/// Gets the opacity of the image to blend
@ -65,43 +71,57 @@ internal class DrawImageProcessor<TPixelBg, TPixelFg> : ImageProcessor<TPixelBg>
/// <summary>
/// Gets the location to draw the blended image
/// </summary>
public Point Location { get; }
public Point BackgroundLocation { get; }
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixelBg> source)
{
Rectangle sourceRectangle = this.SourceRectangle;
Configuration configuration = this.Configuration;
Image<TPixelFg> targetImage = this.Image;
PixelBlender<TPixelBg> blender = this.Blender;
int locationY = this.Location.Y;
// Align the bounds so that both the source and targets are the same width and height for blending.
// We ensure that negative locations are subtracted from both bounds so that foreground images can partially overlap.
Rectangle foregroundRectangle = this.ForegroundRectangle;
// Align start/end positions.
Rectangle bounds = targetImage.Bounds;
// Sanitize the location so that we don't try and sample outside the image.
int left = this.BackgroundLocation.X;
int top = this.BackgroundLocation.Y;
int minX = Math.Max(this.Location.X, sourceRectangle.X);
int maxX = Math.Min(this.Location.X + bounds.Width, sourceRectangle.Right);
int targetX = minX - this.Location.X;
int minY = Math.Max(this.Location.Y, sourceRectangle.Y);
int maxY = Math.Min(this.Location.Y + bounds.Height, sourceRectangle.Bottom);
int width = maxX - minX;
if (this.BackgroundLocation.X < 0)
{
foregroundRectangle.Width += this.BackgroundLocation.X;
left = 0;
}
Rectangle workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY);
if (this.BackgroundLocation.Y < 0)
{
foregroundRectangle.Height += this.BackgroundLocation.Y;
top = 0;
}
// Not a valid operation because rectangle does not overlap with this image.
if (workingRect.Width <= 0 || workingRect.Height <= 0)
int width = foregroundRectangle.Width;
int height = foregroundRectangle.Height;
if (width <= 0 || height <= 0)
{
throw new ImageProcessingException(
"Cannot draw image because the source image does not overlap the target image.");
// Nothing to do, return.
return;
}
DrawImageProcessor<TPixelBg, TPixelFg>.RowOperation operation = new(source.PixelBuffer, targetImage.Frames.RootFrame.PixelBuffer, blender, configuration, minX, width, locationY, targetX, this.Opacity);
// Sanitize the dimensions so that we don't try and sample outside the image.
foregroundRectangle = Rectangle.Intersect(foregroundRectangle, this.ForegroundImage.Bounds);
Rectangle backgroundRectangle = Rectangle.Intersect(new(left, top, width, height), this.SourceRectangle);
Configuration configuration = this.Configuration;
DrawImageProcessor<TPixelBg, TPixelFg>.RowOperation operation =
new(
configuration,
source.PixelBuffer,
this.ForegroundImage.Frames.RootFrame.PixelBuffer,
backgroundRectangle,
foregroundRectangle,
this.Blender,
this.Opacity);
ParallelRowIterator.IterateRows(
configuration,
workingRect,
new(0, 0, foregroundRectangle.Width, foregroundRectangle.Height),
in operation);
}
@ -110,36 +130,30 @@ internal class DrawImageProcessor<TPixelBg, TPixelFg> : ImageProcessor<TPixelBg>
/// </summary>
private readonly struct RowOperation : IRowOperation
{
private readonly Buffer2D<TPixelBg> source;
private readonly Buffer2D<TPixelFg> target;
private readonly Buffer2D<TPixelBg> background;
private readonly Buffer2D<TPixelFg> foreground;
private readonly PixelBlender<TPixelBg> blender;
private readonly Configuration configuration;
private readonly int minX;
private readonly int width;
private readonly int locationY;
private readonly int targetX;
private readonly Rectangle foregroundRectangle;
private readonly Rectangle backgroundRectangle;
private readonly float opacity;
[MethodImpl(InliningOptions.ShortMethod)]
public RowOperation(
Buffer2D<TPixelBg> source,
Buffer2D<TPixelFg> target,
PixelBlender<TPixelBg> blender,
Configuration configuration,
int minX,
int width,
int locationY,
int targetX,
Buffer2D<TPixelBg> background,
Buffer2D<TPixelFg> foreground,
Rectangle backgroundRectangle,
Rectangle foregroundRectangle,
PixelBlender<TPixelBg> blender,
float opacity)
{
this.source = source;
this.target = target;
this.blender = blender;
this.configuration = configuration;
this.minX = minX;
this.width = width;
this.locationY = locationY;
this.targetX = targetX;
this.background = background;
this.foreground = foreground;
this.backgroundRectangle = backgroundRectangle;
this.foregroundRectangle = foregroundRectangle;
this.blender = blender;
this.opacity = opacity;
}
@ -147,8 +161,8 @@ internal class DrawImageProcessor<TPixelBg, TPixelFg> : ImageProcessor<TPixelBg>
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int y)
{
Span<TPixelBg> background = this.source.DangerousGetRowSpan(y).Slice(this.minX, this.width);
Span<TPixelFg> foreground = this.target.DangerousGetRowSpan(y - this.locationY).Slice(this.targetX, this.width);
Span<TPixelBg> background = this.background.DangerousGetRowSpan(y + this.backgroundRectangle.Top).Slice(this.backgroundRectangle.Left, this.backgroundRectangle.Width);
Span<TPixelFg> foreground = this.foreground.DangerousGetRowSpan(y + this.foregroundRectangle.Top).Slice(this.foregroundRectangle.Left, this.foregroundRectangle.Width);
this.blender.Blend<TPixelFg>(this.configuration, background, background, foreground, this.opacity);
}
}

47
src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor{TPixel}.cs

@ -4,6 +4,7 @@
using System.Buffers;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@ -34,17 +35,25 @@ internal class OilPaintingProcessor<TPixel> : ImageProcessor<TPixel>
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source)
{
int levels = Math.Clamp(this.definition.Levels, 1, 255);
int brushSize = Math.Clamp(this.definition.BrushSize, 1, Math.Min(source.Width, source.Height));
using Buffer2D<TPixel> targetPixels = this.Configuration.MemoryAllocator.Allocate2D<TPixel>(source.Size());
source.CopyTo(targetPixels);
RowIntervalOperation operation = new(this.SourceRectangle, targetPixels, source.PixelBuffer, this.Configuration, brushSize >> 1, this.definition.Levels);
ParallelRowIterator.IterateRowIntervals(
RowIntervalOperation operation = new(this.SourceRectangle, targetPixels, source.PixelBuffer, this.Configuration, brushSize >> 1, levels);
try
{
ParallelRowIterator.IterateRowIntervals(
this.Configuration,
this.SourceRectangle,
in operation);
}
catch (Exception ex)
{
throw new ImageProcessingException("The OilPaintProcessor failed. The most likely reason is that a pixel component was outside of its' allowed range.", ex);
}
Buffer2D<TPixel>.SwapOrCopyContent(source.PixelBuffer, targetPixels);
}
@ -105,18 +114,18 @@ internal class OilPaintingProcessor<TPixel> : ImageProcessor<TPixel>
Span<Vector4> targetRowVector4Span = targetRowBuffer.Memory.Span;
Span<Vector4> targetRowAreaVector4Span = targetRowVector4Span.Slice(this.bounds.X, this.bounds.Width);
ref float binsRef = ref bins.GetReference();
ref int intensityBinRef = ref Unsafe.As<float, int>(ref binsRef);
ref float redBinRef = ref Unsafe.Add(ref binsRef, (uint)this.levels);
ref float blueBinRef = ref Unsafe.Add(ref redBinRef, (uint)this.levels);
ref float greenBinRef = ref Unsafe.Add(ref blueBinRef, (uint)this.levels);
Span<float> binsSpan = bins.GetSpan();
Span<int> intensityBinsSpan = MemoryMarshal.Cast<float, int>(binsSpan);
Span<float> redBinSpan = binsSpan[this.levels..];
Span<float> blueBinSpan = redBinSpan[this.levels..];
Span<float> greenBinSpan = blueBinSpan[this.levels..];
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> sourceRowPixelSpan = this.source.DangerousGetRowSpan(y);
Span<TPixel> sourceRowAreaPixelSpan = sourceRowPixelSpan.Slice(this.bounds.X, this.bounds.Width);
PixelOperations<TPixel>.Instance.ToVector4(this.configuration, sourceRowAreaPixelSpan, sourceRowAreaVector4Span);
PixelOperations<TPixel>.Instance.ToVector4(this.configuration, sourceRowAreaPixelSpan, sourceRowAreaVector4Span, PixelConversionModifiers.Scale);
for (int x = this.bounds.X; x < this.bounds.Right; x++)
{
@ -140,7 +149,7 @@ internal class OilPaintingProcessor<TPixel> : ImageProcessor<TPixel>
int offsetX = x + fxr;
offsetX = Numerics.Clamp(offsetX, 0, maxX);
Vector4 vector = sourceOffsetRow[offsetX].ToVector4();
Vector4 vector = sourceOffsetRow[offsetX].ToScaledVector4();
float sourceRed = vector.X;
float sourceBlue = vector.Z;
@ -148,21 +157,21 @@ internal class OilPaintingProcessor<TPixel> : ImageProcessor<TPixel>
int currentIntensity = (int)MathF.Round((sourceBlue + sourceGreen + sourceRed) / 3F * (this.levels - 1));
Unsafe.Add(ref intensityBinRef, (uint)currentIntensity)++;
Unsafe.Add(ref redBinRef, (uint)currentIntensity) += sourceRed;
Unsafe.Add(ref blueBinRef, (uint)currentIntensity) += sourceBlue;
Unsafe.Add(ref greenBinRef, (uint)currentIntensity) += sourceGreen;
intensityBinsSpan[currentIntensity]++;
redBinSpan[currentIntensity] += sourceRed;
blueBinSpan[currentIntensity] += sourceBlue;
greenBinSpan[currentIntensity] += sourceGreen;
if (Unsafe.Add(ref intensityBinRef, (uint)currentIntensity) > maxIntensity)
if (intensityBinsSpan[currentIntensity] > maxIntensity)
{
maxIntensity = Unsafe.Add(ref intensityBinRef, (uint)currentIntensity);
maxIntensity = intensityBinsSpan[currentIntensity];
maxIndex = currentIntensity;
}
}
float red = MathF.Abs(Unsafe.Add(ref redBinRef, (uint)maxIndex) / maxIntensity);
float blue = MathF.Abs(Unsafe.Add(ref blueBinRef, (uint)maxIndex) / maxIntensity);
float green = MathF.Abs(Unsafe.Add(ref greenBinRef, (uint)maxIndex) / maxIntensity);
float red = redBinSpan[maxIndex] / maxIntensity;
float blue = blueBinSpan[maxIndex] / maxIntensity;
float green = greenBinSpan[maxIndex] / maxIntensity;
float alpha = sourceRowVector4Span[x].W;
targetRowVector4Span[x] = new Vector4(red, green, blue, alpha);
@ -171,7 +180,7 @@ internal class OilPaintingProcessor<TPixel> : ImageProcessor<TPixel>
Span<TPixel> targetRowAreaPixelSpan = this.targetPixels.DangerousGetRowSpan(y).Slice(this.bounds.X, this.bounds.Width);
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, targetRowAreaVector4Span, targetRowAreaPixelSpan);
PixelOperations<TPixel>.Instance.FromVector4Destructive(this.configuration, targetRowAreaVector4Span, targetRowAreaPixelSpan, PixelConversionModifiers.Scale);
}
}
}

4
src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs

@ -149,13 +149,13 @@ internal sealed class EuclideanPixelMap<TPixel> : IDisposable
/// The granularity of the cache has been determined based upon the current
/// suite of test images and provides the lowest possible memory usage while
/// providing enough match accuracy.
/// Entry count is currently limited to 1185921 entries (2371842 bytes ~2.26MB).
/// Entry count is currently limited to 2335905 entries (4671810 bytes ~4.45MB).
/// </para>
/// </remarks>
private unsafe struct ColorDistanceCache : IDisposable
{
private const int IndexBits = 5;
private const int IndexAlphaBits = 5;
private const int IndexAlphaBits = 6;
private const int IndexCount = (1 << IndexBits) + 1;
private const int IndexAlphaCount = (1 << IndexAlphaBits) + 1;
private const int RgbShift = 8 - IndexBits;

2
tests/Directory.Build.targets

@ -30,7 +30,7 @@
<PackageReference Update="PhotoSauce.MagicScaler" Version="0.12.1" />
<PackageReference Update="Pfim" Version="0.9.1" />
<PackageReference Update="runtime.osx.10.10-x64.CoreCompat.System.Drawing" Version="5.8.64" Condition="'$(IsOSX)'=='true'" />
<PackageReference Update="SharpZipLib" Version="1.3.2" />
<PackageReference Update="SharpZipLib" Version="1.4.2" />
<PackageReference Update="SkiaSharp" Version="2.80.2" />
<PackageReference Update="System.Drawing.Common" Version="6.0.0" />
<PackageReference Update="System.IO.Compression" Version="4.3.0" />

19
tests/ImageSharp.Benchmarks/Processing/OilPaint.cs

@ -0,0 +1,19 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using BenchmarkDotNet.Attributes;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
namespace SixLabors.ImageSharp.Benchmarks.Processing;
[Config(typeof(Config.MultiFramework))]
public class OilPaint
{
[Benchmark]
public void DoOilPaint()
{
using Image<RgbaVector> image = new Image<RgbaVector>(1920, 1200, new(127, 191, 255));
image.Mutate(ctx => ctx.OilPaint());
}
}

2
tests/ImageSharp.Tests/ConfigurationTests.cs

@ -20,7 +20,7 @@ public class ConfigurationTests
public Configuration DefaultConfiguration { get; }
private readonly int expectedDefaultConfigurationCount = 8;
private readonly int expectedDefaultConfigurationCount = 9;
public ConfigurationTests()
{

20
tests/ImageSharp.Tests/Drawing/DrawImageExtensionsTests.cs

@ -13,11 +13,12 @@ public class DrawImageExtensionsTests : BaseImageOperationsExtensionTest
[Fact]
public void DrawImage_OpacityOnly_VerifyGraphicOptionsTakenFromContext()
{
// non-default values as we cant easly defect usage otherwise
// non-default values as we cant easily defect usage otherwise
this.options.AlphaCompositionMode = PixelAlphaCompositionMode.Xor;
this.options.ColorBlendingMode = PixelColorBlendingMode.Screen;
this.operations.DrawImage(null, 0.5f);
using Image<Rgba32> image = new(Configuration.Default, 1, 1);
this.operations.DrawImage(image, 0.5f);
DrawImageProcessor dip = this.Verify<DrawImageProcessor>();
Assert.Equal(0.5, dip.Opacity);
@ -28,11 +29,12 @@ public class DrawImageExtensionsTests : BaseImageOperationsExtensionTest
[Fact]
public void DrawImage_OpacityAndBlending_VerifyGraphicOptionsTakenFromContext()
{
// non-default values as we cant easly defect usage otherwise
// non-default values as we cant easily defect usage otherwise
this.options.AlphaCompositionMode = PixelAlphaCompositionMode.Xor;
this.options.ColorBlendingMode = PixelColorBlendingMode.Screen;
this.operations.DrawImage(null, PixelColorBlendingMode.Multiply, 0.5f);
using Image<Rgba32> image = new(Configuration.Default, 1, 1);
this.operations.DrawImage(image, PixelColorBlendingMode.Multiply, 0.5f);
DrawImageProcessor dip = this.Verify<DrawImageProcessor>();
Assert.Equal(0.5, dip.Opacity);
@ -43,11 +45,12 @@ public class DrawImageExtensionsTests : BaseImageOperationsExtensionTest
[Fact]
public void DrawImage_LocationAndOpacity_VerifyGraphicOptionsTakenFromContext()
{
// non-default values as we cant easly defect usage otherwise
// non-default values as we cant easily defect usage otherwise
this.options.AlphaCompositionMode = PixelAlphaCompositionMode.Xor;
this.options.ColorBlendingMode = PixelColorBlendingMode.Screen;
this.operations.DrawImage(null, Point.Empty, 0.5f);
using Image<Rgba32> image = new(Configuration.Default, 1, 1);
this.operations.DrawImage(image, Point.Empty, 0.5f);
DrawImageProcessor dip = this.Verify<DrawImageProcessor>();
Assert.Equal(0.5, dip.Opacity);
@ -58,11 +61,12 @@ public class DrawImageExtensionsTests : BaseImageOperationsExtensionTest
[Fact]
public void DrawImage_LocationAndOpacityAndBlending_VerifyGraphicOptionsTakenFromContext()
{
// non-default values as we cant easly defect usage otherwise
// non-default values as we cant easily defect usage otherwise
this.options.AlphaCompositionMode = PixelAlphaCompositionMode.Xor;
this.options.ColorBlendingMode = PixelColorBlendingMode.Screen;
this.operations.DrawImage(null, Point.Empty, PixelColorBlendingMode.Multiply, 0.5f);
using Image<Rgba32> image = new(Configuration.Default, 1, 1);
this.operations.DrawImage(image, Point.Empty, PixelColorBlendingMode.Multiply, 0.5f);
DrawImageProcessor dip = this.Verify<DrawImageProcessor>();
Assert.Equal(0.5, dip.Opacity);

67
tests/ImageSharp.Tests/Drawing/DrawImageTests.cs

@ -190,18 +190,65 @@ public class DrawImageTests
}
[Theory]
[WithSolidFilledImages(100, 100, 255, 255, 255, PixelTypes.Rgba32, -30, -30)]
[WithSolidFilledImages(100, 100, 255, 255, 255, PixelTypes.Rgba32, 130, -30)]
[WithSolidFilledImages(100, 100, 255, 255, 255, PixelTypes.Rgba32, 130, 130)]
[WithSolidFilledImages(100, 100, 255, 255, 255, PixelTypes.Rgba32, -30, 130)]
public void NonOverlappingImageThrows(TestImageProvider<Rgba32> provider, int x, int y)
[WithFile(TestImages.Png.Issue2447, PixelTypes.Rgba32)]
public void Issue2447_A<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<Rgba32> background = provider.GetImage();
using Image<Rgba32> overlay = new(Configuration.Default, 10, 10, Color.Black);
ImageProcessingException ex = Assert.Throws<ImageProcessingException>(Test);
using Image<TPixel> foreground = provider.GetImage();
using Image<Rgba32> background = new(100, 100, new Rgba32(0, 255, 255));
Assert.Contains("does not overlap", ex.ToString());
background.Mutate(c => c.DrawImage(foreground, new Point(64, 10), new Rectangle(32, 32, 32, 32), 1F));
void Test() => background.Mutate(context => context.DrawImage(overlay, new Point(x, y), new GraphicsOptions()));
background.DebugSave(
provider,
appendPixelTypeToFileName: false,
appendSourceFileOrDescription: false);
background.CompareToReferenceOutput(
provider,
appendPixelTypeToFileName: false,
appendSourceFileOrDescription: false);
}
[Theory]
[WithFile(TestImages.Png.Issue2447, PixelTypes.Rgba32)]
public void Issue2447_B<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> foreground = provider.GetImage();
using Image<Rgba32> background = new(100, 100, new Rgba32(0, 255, 255));
background.Mutate(c => c.DrawImage(foreground, new Point(10, 10), new Rectangle(320, 128, 32, 32), 1F));
background.DebugSave(
provider,
appendPixelTypeToFileName: false,
appendSourceFileOrDescription: false);
background.CompareToReferenceOutput(
provider,
appendPixelTypeToFileName: false,
appendSourceFileOrDescription: false);
}
[Theory]
[WithFile(TestImages.Png.Issue2447, PixelTypes.Rgba32)]
public void Issue2447_C<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> foreground = provider.GetImage();
using Image<Rgba32> background = new(100, 100, new Rgba32(0, 255, 255));
background.Mutate(c => c.DrawImage(foreground, new Point(10, 10), new Rectangle(32, 32, 32, 32), 1F));
background.DebugSave(
provider,
appendPixelTypeToFileName: false,
appendSourceFileOrDescription: false);
background.CompareToReferenceOutput(
provider,
appendPixelTypeToFileName: false,
appendSourceFileOrDescription: false);
}
}

24
tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs

@ -370,6 +370,30 @@ public class BmpEncoderTests
TestBmpEncoderCore(provider, bitsPerPixel);
}
[Theory]
[WithFile(BlackWhitePalletDataMatrix, PixelTypes.Rgb24, BmpBitsPerPixel.Pixel1)]
public void Encode_Issue2467<TPixel>(TestImageProvider<TPixel> provider, BmpBitsPerPixel bitsPerPixel)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage();
using var reencodedStream = new MemoryStream();
var encoder = new BmpEncoder
{
BitsPerPixel = bitsPerPixel,
SupportTransparency = false,
Quantizer = KnownQuantizers.Octree
};
image.SaveAsBmp(reencodedStream, encoder);
reencodedStream.Seek(0, SeekOrigin.Begin);
using Image<TPixel> reencodedImage = Image.Load<TPixel>(reencodedStream);
reencodedImage.DebugSave(provider);
reencodedImage.CompareToOriginal(provider);
}
private static void TestBmpEncoderCore<TPixel>(
TestImageProvider<TPixel> provider,
BmpBitsPerPixel bitsPerPixel,

7
tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs

@ -1,7 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics.X86;
using Microsoft.DotNet.RemoteExecutor;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Gif;
@ -51,10 +51,11 @@ public class GifDecoderTests
image.DebugSave(provider, testOutputDetails: details, appendPixelTypeToFileName: false);
// Floating point differences result in minor pixel differences.
// Floating point differences in FMA used in the ResizeKernel result in minor pixel differences.
// Output have been manually verified.
// For more details see discussion: https://github.com/SixLabors/ImageSharp/pull/1513#issuecomment-763643594
image.CompareToReferenceOutput(
ImageComparer.TolerantPercentage(TestEnvironment.OSArchitecture == Architecture.Arm64 ? 0.0002F : 0.0001F),
ImageComparer.TolerantPercentage(Fma.IsSupported ? 0.0001F : 0.0002F),
provider,
testOutputDetails: details,
appendPixelTypeToFileName: false);

28
tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs

@ -314,4 +314,32 @@ public partial class JpegDecoderTests
image.DebugSave(provider);
image.CompareToOriginal(provider);
}
// https://github.com/SixLabors/ImageSharp/issues/2478
[Theory]
[WithFile(TestImages.Jpeg.Issues.Issue2478_JFXX, PixelTypes.Rgba32)]
public void Issue2478_DecodeWorks<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(JpegDecoder.Instance);
image.DebugSave(provider);
image.CompareToOriginal(provider);
}
[Theory]
[WithFile(TestImages.Jpeg.Issues.HangBadScan, PixelTypes.L8)]
public void DecodeHang<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
if (TestEnvironment.IsWindows &&
TestEnvironment.RunsOnCI)
{
// Windows CI runs consistently fail with OOM.
return;
}
using Image<TPixel> image = provider.GetImage(JpegDecoder.Instance);
Assert.Equal(65503, image.Width);
Assert.Equal(65503, image.Height);
}
}

1
tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs

@ -81,6 +81,7 @@ public class PbmDecoderTests
[Theory]
[WithFile(BlackAndWhitePlain, PixelTypes.L8, "pbm")]
[WithFile(BlackAndWhiteBinary, PixelTypes.L8, "pbm")]
[WithFile(Issue2477, PixelTypes.L8, "pbm")]
[WithFile(GrayscalePlain, PixelTypes.L8, "pgm")]
[WithFile(GrayscalePlainNormalized, PixelTypes.L8, "pgm")]
[WithFile(GrayscaleBinary, PixelTypes.L8, "pgm")]

6
tests/ImageSharp.Tests/Formats/Pbm/PbmEncoderTests.cs

@ -26,6 +26,7 @@ public class PbmEncoderTests
{
{ BlackAndWhiteBinary, PbmColorType.BlackAndWhite },
{ BlackAndWhitePlain, PbmColorType.BlackAndWhite },
{ Issue2477, PbmColorType.BlackAndWhite },
{ GrayscaleBinary, PbmColorType.Grayscale },
{ GrayscaleBinaryWide, PbmColorType.Grayscale },
{ GrayscalePlain, PbmColorType.Grayscale },
@ -96,6 +97,11 @@ public class PbmEncoderTests
public void PbmEncoder_P4_Works<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestPbmEncoderCore(provider, PbmColorType.BlackAndWhite, PbmEncoding.Binary);
[Theory]
[WithFile(Issue2477, PixelTypes.Rgb24)]
public void PbmEncoder_P4_Irregular_Works<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestPbmEncoderCore(provider, PbmColorType.BlackAndWhite, PbmEncoding.Binary);
[Theory]
[WithFile(GrayscalePlainMagick, PixelTypes.Rgb24)]
public void PbmEncoder_P2_Works<TPixel>(TestImageProvider<TPixel> provider)

11
tests/ImageSharp.Tests/Formats/Pbm/PbmMetadataTests.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Pbm;
using static SixLabors.ImageSharp.Tests.TestImages.Pbm;
@ -80,4 +81,14 @@ public class PbmMetadataTests
Assert.NotNull(bitmapMetadata);
Assert.Equal(expectedComponentType, bitmapMetadata.ComponentType);
}
[Fact]
public void Identify_HandlesCraftedDenialOfServiceString()
{
byte[] bytes = Convert.FromBase64String("UDEjWAAACQAAAAA=");
ImageInfo info = Image.Identify(bytes);
Assert.Equal(default, info.Size);
Configuration.Default.ImageFormatsManager.TryFindFormatByFileExtension("pbm", out IImageFormat format);
Assert.Equal(format!, info.Metadata.DecodedImageFormat);
}
}

45
tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs

@ -1,12 +1,13 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics.X86;
using Microsoft.DotNet.RemoteExecutor;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
using SixLabors.ImageSharp.Tests.TestUtilities;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs;
@ -122,15 +123,52 @@ public partial class PngDecoderTests
image.DebugSave(provider, testOutputDetails: details, appendPixelTypeToFileName: false);
// Floating point differences result in minor pixel differences.
// Floating point differences in FMA used in the ResizeKernel result in minor pixel differences.
// Output have been manually verified.
// For more details see discussion: https://github.com/SixLabors/ImageSharp/pull/1513#issuecomment-763643594
image.CompareToReferenceOutput(
ImageComparer.TolerantPercentage(TestEnvironment.OSArchitecture == Architecture.Arm64 ? 0.0005F : 0.0003F),
ImageComparer.TolerantPercentage(Fma.IsSupported ? 0.0003F : 0.0005F),
provider,
testOutputDetails: details,
appendPixelTypeToFileName: false);
}
[Theory]
[WithFile(TestImages.Png.Splash, PixelTypes.Rgba32)]
public void PngDecoder_Decode_Resize_ScalarResizeKernel(TestImageProvider<Rgba32> provider)
{
HwIntrinsics intrinsicsFilter = HwIntrinsics.DisableHWIntrinsic;
FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest,
intrinsicsFilter,
provider,
string.Empty);
static void RunTest(string arg1, string notUsed)
{
TestImageProvider<Rgba32> provider =
FeatureTestRunner.DeserializeForXunit<TestImageProvider<Rgba32>>(arg1);
DecoderOptions options = new()
{
TargetSize = new() { Width = 150, Height = 150 }
};
using Image<Rgba32> image = provider.GetImage(PngDecoder.Instance, options);
FormattableString details = $"{options.TargetSize.Value.Width}_{options.TargetSize.Value.Height}";
image.DebugSave(provider, testOutputDetails: details, appendPixelTypeToFileName: false);
image.CompareToReferenceOutput(
ImageComparer.TolerantPercentage(0.0005F),
provider,
testOutputDetails: details,
appendPixelTypeToFileName: false);
}
}
[Theory]
[WithFile(TestImages.Png.AverageFilter3BytesPerPixel, PixelTypes.Rgba32)]
[WithFile(TestImages.Png.AverageFilter4BytesPerPixel, PixelTypes.Rgba32)]
@ -500,7 +538,6 @@ public partial class PngDecoderTests
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(PngDecoder.Instance);
image.DebugSave(provider);
PngMetadata metadata = image.Metadata.GetPngMetadata();
Assert.True(metadata.HasTransparency);
}

64
tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs

@ -129,8 +129,8 @@ public partial class PngEncoderTests
PngFilterMethod.Adaptive,
PngBitDepth.Bit8,
interlaceMode,
appendPixelType: true,
appendPngColorType: true);
appendPngColorType: true,
appendPixelType: true);
}
}
@ -321,7 +321,7 @@ public partial class PngEncoderTests
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage();
using var ms = new MemoryStream();
using MemoryStream ms = new();
image.Save(ms, PngEncoder);
byte[] data = ms.ToArray().Take(8).ToArray();
@ -344,13 +344,13 @@ public partial class PngEncoderTests
[MemberData(nameof(RatioFiles))]
public void Encode_PreserveRatio(string imagePath, int xResolution, int yResolution, PixelResolutionUnit resolutionUnit)
{
var testFile = TestFile.Create(imagePath);
TestFile testFile = TestFile.Create(imagePath);
using Image<Rgba32> input = testFile.CreateRgba32Image();
using var memStream = new MemoryStream();
using MemoryStream memStream = new();
input.Save(memStream, PngEncoder);
memStream.Position = 0;
using var output = Image.Load<Rgba32>(memStream);
using Image<Rgba32> output = Image.Load<Rgba32>(memStream);
ImageMetadata meta = output.Metadata;
Assert.Equal(xResolution, meta.HorizontalResolution);
Assert.Equal(yResolution, meta.VerticalResolution);
@ -361,13 +361,13 @@ public partial class PngEncoderTests
[MemberData(nameof(PngBitDepthFiles))]
public void Encode_PreserveBits(string imagePath, PngBitDepth pngBitDepth)
{
var testFile = TestFile.Create(imagePath);
TestFile testFile = TestFile.Create(imagePath);
using Image<Rgba32> input = testFile.CreateRgba32Image();
using var memStream = new MemoryStream();
using MemoryStream memStream = new();
input.Save(memStream, PngEncoder);
memStream.Position = 0;
using var output = Image.Load<Rgba32>(memStream);
using Image<Rgba32> output = Image.Load<Rgba32>(memStream);
PngMetadata meta = output.Metadata.GetPngMetadata();
Assert.Equal(pngBitDepth, meta.BitDepth);
@ -380,8 +380,8 @@ public partial class PngEncoderTests
public void Encode_WithPngTransparentColorBehaviorClear_Works(PngColorType colorType)
{
// arrange
var image = new Image<Rgba32>(50, 50);
var encoder = new PngEncoder()
Image<Rgba32> image = new(50, 50);
PngEncoder encoder = new()
{
TransparentColorMode = PngTransparentColorMode.Clear,
ColorType = colorType
@ -391,7 +391,7 @@ public partial class PngEncoderTests
{
for (int y = 0; y < image.Height; y++)
{
System.Span<Rgba32> rowSpan = accessor.GetRowSpan(y);
Span<Rgba32> rowSpan = accessor.GetRowSpan(y);
// Half of the test image should be transparent.
if (y > 25)
@ -407,12 +407,12 @@ public partial class PngEncoderTests
});
// act
using var memStream = new MemoryStream();
using MemoryStream memStream = new();
image.Save(memStream, encoder);
// assert
memStream.Position = 0;
using var actual = Image.Load<Rgba32>(memStream);
using Image<Rgba32> actual = Image.Load<Rgba32>(memStream);
Rgba32 expectedColor = Color.Blue;
if (colorType is PngColorType.Grayscale or PngColorType.GrayscaleWithAlpha)
{
@ -424,7 +424,7 @@ public partial class PngEncoderTests
{
for (int y = 0; y < accessor.Height; y++)
{
System.Span<Rgba32> rowSpan = accessor.GetRowSpan(y);
Span<Rgba32> rowSpan = accessor.GetRowSpan(y);
if (y > 25)
{
@ -443,15 +443,15 @@ public partial class PngEncoderTests
[MemberData(nameof(PngTrnsFiles))]
public void Encode_PreserveTrns(string imagePath, PngBitDepth pngBitDepth, PngColorType pngColorType)
{
var testFile = TestFile.Create(imagePath);
TestFile testFile = TestFile.Create(imagePath);
using Image<Rgba32> input = testFile.CreateRgba32Image();
PngMetadata inMeta = input.Metadata.GetPngMetadata();
Assert.True(inMeta.HasTransparency);
using var memStream = new MemoryStream();
using MemoryStream memStream = new();
input.Save(memStream, PngEncoder);
memStream.Position = 0;
using var output = Image.Load<Rgba32>(memStream);
using Image<Rgba32> output = Image.Load<Rgba32>(memStream);
PngMetadata outMeta = output.Metadata.GetPngMetadata();
Assert.True(outMeta.HasTransparency);
@ -501,8 +501,8 @@ public partial class PngEncoderTests
PngFilterMethod.Adaptive,
PngBitDepth.Bit8,
interlaceMode,
appendPixelType: true,
appendPngColorType: true);
appendPngColorType: true,
appendPixelType: true);
}
}
@ -523,8 +523,8 @@ public partial class PngEncoderTests
PngFilterMethod.Adaptive,
PngBitDepth.Bit8,
interlaceMode,
appendPixelType: true,
appendPngColorType: true);
appendPngColorType: true,
appendPixelType: true);
}
}
@ -538,13 +538,27 @@ public partial class PngEncoderTests
public void EncodeFixesInvalidOptions()
{
// https://github.com/SixLabors/ImageSharp/issues/935
using var ms = new MemoryStream();
var testFile = TestFile.Create(TestImages.Png.Issue935);
using MemoryStream ms = new();
TestFile testFile = TestFile.Create(TestImages.Png.Issue935);
using Image<Rgba32> image = testFile.CreateRgba32Image(PngDecoder.Instance);
image.Save(ms, new PngEncoder { ColorType = PngColorType.RgbWithAlpha });
}
// https://github.com/SixLabors/ImageSharp/issues/2469
[Theory]
[WithFile(TestImages.Png.Issue2469, PixelTypes.Rgba32)]
public void Issue2469_Quantized_Encode_Artifacts<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(PngDecoder.Instance);
PngEncoder encoder = new() { BitDepth = PngBitDepth.Bit8, ColorType = PngColorType.Palette };
string actualOutputFile = provider.Utility.SaveTestOutputFile(image, "png", encoder);
using Image<Rgba32> encoded = Image.Load<Rgba32>(actualOutputFile);
encoded.CompareToReferenceOutput(ImageComparer.Exact, provider);
}
private static void TestPngEncoderCore<TPixel>(
TestImageProvider<TPixel> provider,
PngColorType pngColorType,
@ -563,7 +577,7 @@ public partial class PngEncoderTests
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage();
var encoder = new PngEncoder
PngEncoder encoder = new()
{
ColorType = pngColorType,
FilterMethod = pngFilterMethod,

135
tests/ImageSharp.Tests/Formats/Qoi/ImageExtensionsTest.cs

@ -0,0 +1,135 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Qoi;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Tests.Formats.Qoi;
public class ImageExtensionsTest
{
[Fact]
public void SaveAsQoi_Path()
{
string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensionsTest));
string file = Path.Combine(dir, "SaveAsQoi_Path.qoi");
using (Image<L8> image = new(10, 10))
{
image.SaveAsQoi(file);
}
IImageFormat format = Image.DetectFormat(file);
Assert.True(format is QoiFormat);
}
[Fact]
public async Task SaveAsQoiAsync_Path()
{
string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensionsTest));
string file = Path.Combine(dir, "SaveAsQoiAsync_Path.qoi");
using (Image<L8> image = new(10, 10))
{
await image.SaveAsQoiAsync(file);
}
IImageFormat format = Image.DetectFormat(file);
Assert.True(format is QoiFormat);
}
[Fact]
public void SaveAsQoi_Path_Encoder()
{
string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensions));
string file = Path.Combine(dir, "SaveAsQoi_Path_Encoder.qoi");
using (Image<L8> image = new(10, 10))
{
image.SaveAsQoi(file, new QoiEncoder());
}
IImageFormat format = Image.DetectFormat(file);
Assert.True(format is QoiFormat);
}
[Fact]
public async Task SaveAsQoiAsync_Path_Encoder()
{
string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensions));
string file = Path.Combine(dir, "SaveAsQoiAsync_Path_Encoder.qoi");
using (Image<L8> image = new(10, 10))
{
await image.SaveAsQoiAsync(file, new QoiEncoder());
}
IImageFormat format = Image.DetectFormat(file);
Assert.True(format is QoiFormat);
}
[Fact]
public void SaveAsQoi_Stream()
{
using MemoryStream memoryStream = new();
using (Image<L8> image = new(10, 10))
{
image.SaveAsQoi(memoryStream);
}
memoryStream.Position = 0;
IImageFormat format = Image.DetectFormat(memoryStream);
Assert.True(format is QoiFormat);
}
[Fact]
public async Task SaveAsQoiAsync_StreamAsync()
{
using MemoryStream memoryStream = new();
using (Image<L8> image = new(10, 10))
{
await image.SaveAsQoiAsync(memoryStream);
}
memoryStream.Position = 0;
IImageFormat format = Image.DetectFormat(memoryStream);
Assert.True(format is QoiFormat);
}
[Fact]
public void SaveAsQoi_Stream_Encoder()
{
using MemoryStream memoryStream = new();
using (Image<L8> image = new(10, 10))
{
image.SaveAsQoi(memoryStream, new QoiEncoder());
}
memoryStream.Position = 0;
IImageFormat format = Image.DetectFormat(memoryStream);
Assert.True(format is QoiFormat);
}
[Fact]
public async Task SaveAsQoiAsync_Stream_Encoder()
{
using MemoryStream memoryStream = new();
using (Image<L8> image = new(10, 10))
{
await image.SaveAsQoiAsync(memoryStream, new QoiEncoder());
}
memoryStream.Position = 0;
IImageFormat format = Image.DetectFormat(memoryStream);
Assert.True(format is QoiFormat);
}
}

56
tests/ImageSharp.Tests/Formats/Qoi/QoiDecoderTests.cs

@ -0,0 +1,56 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Qoi;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Tests.Formats.Qoi;
[Trait("Format", "Qoi")]
[ValidateDisposedMemoryAllocations]
public class QoiDecoderTests
{
[Theory]
[InlineData(TestImages.Qoi.Dice, QoiChannels.Rgba, QoiColorSpace.SrgbWithLinearAlpha)]
[InlineData(TestImages.Qoi.EdgeCase, QoiChannels.Rgba, QoiColorSpace.SrgbWithLinearAlpha)]
[InlineData(TestImages.Qoi.Kodim10, QoiChannels.Rgb, QoiColorSpace.SrgbWithLinearAlpha)]
[InlineData(TestImages.Qoi.Kodim23, QoiChannels.Rgb, QoiColorSpace.SrgbWithLinearAlpha)]
[InlineData(TestImages.Qoi.QoiLogo, QoiChannels.Rgba, QoiColorSpace.SrgbWithLinearAlpha)]
[InlineData(TestImages.Qoi.TestCard, QoiChannels.Rgba, QoiColorSpace.SrgbWithLinearAlpha)]
[InlineData(TestImages.Qoi.TestCardRGBA, QoiChannels.Rgba, QoiColorSpace.SrgbWithLinearAlpha)]
[InlineData(TestImages.Qoi.Wikipedia008, QoiChannels.Rgb, QoiColorSpace.SrgbWithLinearAlpha)]
public void Identify(string imagePath, QoiChannels channels, QoiColorSpace colorSpace)
{
TestFile testFile = TestFile.Create(imagePath);
using MemoryStream stream = new(testFile.Bytes, false);
ImageInfo imageInfo = Image.Identify(stream);
QoiMetadata qoiMetadata = imageInfo.Metadata.GetQoiMetadata();
Assert.NotNull(imageInfo);
Assert.Equal(imageInfo.Metadata.DecodedImageFormat, QoiFormat.Instance);
Assert.Equal(qoiMetadata.Channels, channels);
Assert.Equal(qoiMetadata.ColorSpace, colorSpace);
}
[Theory]
[WithFile(TestImages.Qoi.Dice, PixelTypes.Rgba32, QoiChannels.Rgba, QoiColorSpace.SrgbWithLinearAlpha)]
[WithFile(TestImages.Qoi.EdgeCase, PixelTypes.Rgba32, QoiChannels.Rgba, QoiColorSpace.SrgbWithLinearAlpha)]
[WithFile(TestImages.Qoi.Kodim10, PixelTypes.Rgba32, QoiChannels.Rgb, QoiColorSpace.SrgbWithLinearAlpha)]
[WithFile(TestImages.Qoi.Kodim23, PixelTypes.Rgba32, QoiChannels.Rgb, QoiColorSpace.SrgbWithLinearAlpha)]
[WithFile(TestImages.Qoi.QoiLogo, PixelTypes.Rgba32, QoiChannels.Rgba, QoiColorSpace.SrgbWithLinearAlpha)]
[WithFile(TestImages.Qoi.TestCard, PixelTypes.Rgba32, QoiChannels.Rgba, QoiColorSpace.SrgbWithLinearAlpha)]
[WithFile(TestImages.Qoi.TestCardRGBA, PixelTypes.Rgba32, QoiChannels.Rgba, QoiColorSpace.SrgbWithLinearAlpha)]
[WithFile(TestImages.Qoi.Wikipedia008, PixelTypes.Rgba32, QoiChannels.Rgb, QoiColorSpace.SrgbWithLinearAlpha)]
public void Decode<TPixel>(TestImageProvider<TPixel> provider, QoiChannels channels, QoiColorSpace colorSpace)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage();
QoiMetadata qoiMetadata = image.Metadata.GetQoiMetadata();
image.DebugSave(provider);
image.CompareToReferenceOutput(provider);
Assert.Equal(qoiMetadata.Channels, channels);
Assert.Equal(qoiMetadata.ColorSpace, colorSpace);
}
}

44
tests/ImageSharp.Tests/Formats/Qoi/QoiEncoderTests.cs

@ -0,0 +1,44 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Qoi;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs;
namespace SixLabors.ImageSharp.Tests.Formats.Qoi;
[Trait("Format", "Qoi")]
[ValidateDisposedMemoryAllocations]
public class QoiEncoderTests
{
[Theory]
[WithFile(TestImages.Qoi.Dice, PixelTypes.Rgba32, QoiChannels.Rgba, QoiColorSpace.SrgbWithLinearAlpha)]
[WithFile(TestImages.Qoi.EdgeCase, PixelTypes.Rgba32, QoiChannels.Rgba, QoiColorSpace.SrgbWithLinearAlpha)]
[WithFile(TestImages.Qoi.Kodim10, PixelTypes.Rgba32, QoiChannels.Rgb, QoiColorSpace.SrgbWithLinearAlpha)]
[WithFile(TestImages.Qoi.Kodim23, PixelTypes.Rgba32, QoiChannels.Rgb, QoiColorSpace.SrgbWithLinearAlpha)]
[WithFile(TestImages.Qoi.QoiLogo, PixelTypes.Rgba32, QoiChannels.Rgba, QoiColorSpace.SrgbWithLinearAlpha)]
[WithFile(TestImages.Qoi.TestCard, PixelTypes.Rgba32, QoiChannels.Rgba, QoiColorSpace.SrgbWithLinearAlpha)]
[WithFile(TestImages.Qoi.TestCardRGBA, PixelTypes.Rgba32, QoiChannels.Rgba, QoiColorSpace.SrgbWithLinearAlpha)]
[WithFile(TestImages.Qoi.Wikipedia008, PixelTypes.Rgba32, QoiChannels.Rgb, QoiColorSpace.SrgbWithLinearAlpha)]
public static void Encode<TPixel>(TestImageProvider<TPixel> provider, QoiChannels channels, QoiColorSpace colorSpace)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(new MagickReferenceDecoder());
using MemoryStream stream = new();
QoiEncoder encoder = new()
{
Channels = channels,
ColorSpace = colorSpace
};
image.Save(stream, encoder);
stream.Position = 0;
using Image<TPixel> encodedImage = (Image<TPixel>)Image.Load(stream);
QoiMetadata qoiMetadata = encodedImage.Metadata.GetQoiMetadata();
ImageComparer.Exact.CompareImages(image, encodedImage);
Assert.Equal(qoiMetadata.Channels, channels);
Assert.Equal(qoiMetadata.ColorSpace, colorSpace);
}
}

6
tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics.X86;
using Microsoft.DotNet.RemoteExecutor;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Tga;
@ -760,10 +761,11 @@ public class TgaDecoderTests
image.DebugSave(provider, testOutputDetails: details, appendPixelTypeToFileName: false);
// Floating point differences result in minor pixel differences.
// Floating point differences in FMA used in the ResizeKernel result in minor pixel differences.
// Output have been manually verified.
// For more details see discussion: https://github.com/SixLabors/ImageSharp/pull/1513#issuecomment-763643594
image.CompareToReferenceOutput(
ImageComparer.TolerantPercentage(TestEnvironment.OSArchitecture == Architecture.Arm64 ? 0.0016F : 0.0001F),
ImageComparer.TolerantPercentage(Fma.IsSupported ? 0.0001F : 0.0016F),
provider,
testOutputDetails: details,
appendPixelTypeToFileName: false);

55
tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs

@ -3,6 +3,7 @@
// ReSharper disable InconsistentNaming
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics.X86;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Tiff;
using SixLabors.ImageSharp.Metadata;
@ -200,11 +201,7 @@ public class TiffDecoderTests : TiffDecoderBaseTester
[Theory]
[WithFile(Rgba3BitAssociatedAlpha, PixelTypes.Rgba32)]
public void TiffDecoder_CanDecode_12Bit_WithAssociatedAlpha<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
TestTiffDecoder(provider, useExactComparer: false, compareTolerance: 0.264F);
}
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider, useExactComparer: false, compareTolerance: 0.264F);
[Theory]
[WithFile(Flower14BitGray, PixelTypes.Rgba32)]
@ -249,11 +246,7 @@ public class TiffDecoderTests : TiffDecoderBaseTester
[WithFile(Rgba5BitAssociatedAlpha, PixelTypes.Rgba32)]
public void TiffDecoder_CanDecode_20Bit_WithAssociatedAlpha<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
TestTiffDecoder(provider, useExactComparer: false, compareTolerance: 0.376F);
}
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider, useExactComparer: false, compareTolerance: 0.376F);
[Theory]
[WithFile(FlowerRgb888Contiguous, PixelTypes.Rgba32)]
@ -268,11 +261,7 @@ public class TiffDecoderTests : TiffDecoderBaseTester
[Theory]
[WithFile(Rgba6BitAssociatedAlpha, PixelTypes.Rgba32)]
public void TiffDecoder_CanDecode_24Bit_WithAssociatedAlpha<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
TestTiffDecoder(provider, useExactComparer: false, compareTolerance: 0.405F);
}
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider, useExactComparer: false, compareTolerance: 0.405F);
[Theory]
[WithFile(Flower24BitGray, PixelTypes.Rgba32)]
@ -319,6 +308,7 @@ public class TiffDecoderTests : TiffDecoderBaseTester
[Theory]
[WithFile(Cmyk, PixelTypes.Rgba32)]
[WithFile(CmykLzwPredictor, PixelTypes.Rgba32)]
public void TiffDecoder_CanDecode_Cmyk<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
@ -362,12 +352,8 @@ public class TiffDecoderTests : TiffDecoderBaseTester
[Theory]
[WithFile(Rgba8BitAssociatedAlpha, PixelTypes.Rgba32)]
public void TiffDecoder_CanDecode_32Bit_WithAssociatedAlpha<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
// Note: Using tolerant comparer here, because there is a small difference to the reference decoder probably due to floating point rounding issues.
TestTiffDecoder(provider, useExactComparer: false, compareTolerance: 0.004F);
}
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider, useExactComparer: false, compareTolerance: 0.004F);
[Theory]
[WithFile(Flower32BitGrayPredictorBigEndian, PixelTypes.Rgba32)]
@ -395,11 +381,7 @@ public class TiffDecoderTests : TiffDecoderBaseTester
[WithFile(Rgba10BitAssociatedAlphaBigEndian, PixelTypes.Rgba32)]
[WithFile(Rgba10BitAssociatedAlphaLittleEndian, PixelTypes.Rgba32)]
public void TiffDecoder_CanDecode_40Bit_WithAssociatedAlpha<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
TestTiffDecoder(provider, useExactComparer: false, compareTolerance: 0.247F);
}
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider, useExactComparer: false, compareTolerance: 0.247F);
[Theory]
[WithFile(FlowerRgb141414Contiguous, PixelTypes.Rgba32)]
@ -426,11 +408,7 @@ public class TiffDecoderTests : TiffDecoderBaseTester
[WithFile(Rgba12BitAssociatedAlphaBigEndian, PixelTypes.Rgba32)]
[WithFile(Rgba12BitAssociatedAlphaLittleEndian, PixelTypes.Rgba32)]
public void TiffDecoder_CanDecode_48Bit_WithAssociatedAlpha<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
TestTiffDecoder(provider, useExactComparer: false, compareTolerance: 0.118F);
}
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider, useExactComparer: false, compareTolerance: 0.118F);
[Theory]
[WithFile(FlowerRgb161616PredictorBigEndian, PixelTypes.Rgba32)]
@ -448,11 +426,7 @@ public class TiffDecoderTests : TiffDecoderBaseTester
[WithFile(Rgba14BitAssociatedAlphaBigEndian, PixelTypes.Rgba32)]
[WithFile(Rgba14BitAssociatedAlphaLittleEndian, PixelTypes.Rgba32)]
public void TiffDecoder_CanDecode_56Bit_WithAssociatedAlpha<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
TestTiffDecoder(provider, useExactComparer: false, compareTolerance: 0.075F);
}
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider, useExactComparer: false, compareTolerance: 0.075F);
[Theory]
[WithFile(FlowerRgb242424Contiguous, PixelTypes.Rgba32)]
@ -686,6 +660,12 @@ public class TiffDecoderTests : TiffDecoderBaseTester
public void TiffDecoder_CanDecode_Fax4CompressedWithStrips<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider);
// https://github.com/SixLabors/ImageSharp/issues/2435
[Theory]
[WithFile(Issues2435, PixelTypes.Rgba32)]
public void TiffDecoder_CanDecode_TiledWithNonEqualWidthAndHeight<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> => TestTiffDecoder(provider);
[Theory]
[WithFileCollection(nameof(MultiframeTestImages), PixelTypes.Rgba32)]
public void DecodeMultiframe<TPixel>(TestImageProvider<TPixel> provider)
@ -717,10 +697,11 @@ public class TiffDecoderTests : TiffDecoderBaseTester
image.DebugSave(provider, testOutputDetails: details, appendPixelTypeToFileName: false);
// Floating point differences result in minor pixel differences.
// Floating point differences in FMA used in the ResizeKernel result in minor pixel differences.
// Output have been manually verified.
// For more details see discussion: https://github.com/SixLabors/ImageSharp/pull/1513#issuecomment-763643594
image.CompareToReferenceOutput(
TestEnvironment.OSArchitecture == Architecture.Arm64 ? ImageComparer.TolerantPercentage(0.0006F) : ImageComparer.Exact,
Fma.IsSupported ? ImageComparer.Exact : ImageComparer.TolerantPercentage(0.0006F),
provider,
testOutputDetails: details,
appendPixelTypeToFileName: false);

235
tests/ImageSharp.Tests/Formats/WebP/Vp8ResidualTests.cs

@ -1,6 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Text;
using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Formats.Webp.Lossy;
using SixLabors.ImageSharp.Tests.TestUtilities;
@ -9,10 +11,234 @@ namespace SixLabors.ImageSharp.Tests.Formats.Webp;
[Trait("Format", "Webp")]
public class Vp8ResidualTests
{
private static void WriteVp8Residual(string filename, Vp8Residual residual)
{
using FileStream stream = File.Open(filename, FileMode.Create);
using BinaryWriter writer = new(stream, Encoding.UTF8, false);
writer.Write(residual.First);
writer.Write(residual.Last);
writer.Write(residual.CoeffType);
for (int i = 0; i < residual.Coeffs.Length; i++)
{
writer.Write(residual.Coeffs[i]);
}
for (int i = 0; i < residual.Prob.Length; i++)
{
for (int j = 0; j < residual.Prob[i].Probabilities.Length; j++)
{
writer.Write(residual.Prob[i].Probabilities[j].Probabilities);
}
}
for (int i = 0; i < residual.Costs.Length; i++)
{
Vp8Costs costs = residual.Costs[i];
Vp8CostArray[] costsArray = costs.Costs;
for (int j = 0; j < costsArray.Length; j++)
{
for (int k = 0; k < costsArray[j].Costs.Length; k++)
{
writer.Write(costsArray[j].Costs[k]);
}
}
}
for (int i = 0; i < residual.Stats.Length; i++)
{
for (int j = 0; j < residual.Stats[i].Stats.Length; j++)
{
for (int k = 0; k < residual.Stats[i].Stats[j].Stats.Length; k++)
{
writer.Write(residual.Stats[i].Stats[j].Stats[k]);
}
}
}
writer.Flush();
}
private static Vp8Residual ReadVp8Residual(string fileName)
{
using FileStream stream = File.Open(fileName, FileMode.Open);
using BinaryReader reader = new(stream, Encoding.UTF8, false);
Vp8Residual residual = new()
{
First = reader.ReadInt32(),
Last = reader.ReadInt32(),
CoeffType = reader.ReadInt32()
};
for (int i = 0; i < residual.Coeffs.Length; i++)
{
residual.Coeffs[i] = reader.ReadInt16();
}
Vp8BandProbas[] bandProbas = new Vp8BandProbas[8];
for (int i = 0; i < bandProbas.Length; i++)
{
bandProbas[i] = new Vp8BandProbas();
for (int j = 0; j < bandProbas[i].Probabilities.Length; j++)
{
for (int k = 0; k < 11; k++)
{
bandProbas[i].Probabilities[j].Probabilities[k] = reader.ReadByte();
}
}
}
residual.Prob = bandProbas;
residual.Costs = new Vp8Costs[16];
for (int i = 0; i < residual.Costs.Length; i++)
{
residual.Costs[i] = new Vp8Costs();
Vp8CostArray[] costsArray = residual.Costs[i].Costs;
for (int j = 0; j < costsArray.Length; j++)
{
for (int k = 0; k < costsArray[j].Costs.Length; k++)
{
costsArray[j].Costs[k] = reader.ReadUInt16();
}
}
}
residual.Stats = new Vp8Stats[8];
for (int i = 0; i < residual.Stats.Length; i++)
{
residual.Stats[i] = new Vp8Stats();
for (int j = 0; j < residual.Stats[i].Stats.Length; j++)
{
for (int k = 0; k < residual.Stats[i].Stats[j].Stats.Length; k++)
{
residual.Stats[i].Stats[j].Stats[k] = reader.ReadUInt32();
}
}
}
return residual;
}
[Fact]
public void Vp8Residual_Serialization_Works()
{
// arrange
Vp8Residual expected = new();
Vp8EncProba encProb = new();
Random rand = new(439);
CreateRandomProbas(encProb, rand);
CreateCosts(encProb, rand);
expected.Init(1, 0, encProb);
for (int i = 0; i < expected.Coeffs.Length; i++)
{
expected.Coeffs[i] = (byte)rand.Next(255);
}
// act
string fileName = "Vp8SerializationTest.bin";
WriteVp8Residual(fileName, expected);
Vp8Residual actual = ReadVp8Residual(fileName);
File.Delete(fileName);
// assert
Assert.Equal(expected.CoeffType, actual.CoeffType);
Assert.Equal(expected.Coeffs, actual.Coeffs);
Assert.Equal(expected.Costs.Length, actual.Costs.Length);
Assert.Equal(expected.First, actual.First);
Assert.Equal(expected.Last, actual.Last);
Assert.Equal(expected.Stats.Length, actual.Stats.Length);
for (int i = 0; i < expected.Stats.Length; i++)
{
Vp8StatsArray[] expectedStats = expected.Stats[i].Stats;
Vp8StatsArray[] actualStats = actual.Stats[i].Stats;
Assert.Equal(expectedStats.Length, actualStats.Length);
for (int j = 0; j < expectedStats.Length; j++)
{
Assert.Equal(expectedStats[j].Stats, actualStats[j].Stats);
}
}
Assert.Equal(expected.Prob.Length, actual.Prob.Length);
for (int i = 0; i < expected.Prob.Length; i++)
{
Vp8ProbaArray[] expectedProbabilities = expected.Prob[i].Probabilities;
Vp8ProbaArray[] actualProbabilities = actual.Prob[i].Probabilities;
Assert.Equal(expectedProbabilities.Length, actualProbabilities.Length);
for (int j = 0; j < expectedProbabilities.Length; j++)
{
Assert.Equal(expectedProbabilities[j].Probabilities, actualProbabilities[j].Probabilities);
}
}
for (int i = 0; i < expected.Costs.Length; i++)
{
Vp8CostArray[] expectedCosts = expected.Costs[i].Costs;
Vp8CostArray[] actualCosts = actual.Costs[i].Costs;
Assert.Equal(expectedCosts.Length, actualCosts.Length);
for (int j = 0; j < expectedCosts.Length; j++)
{
Assert.Equal(expectedCosts[j].Costs, actualCosts[j].Costs);
}
}
}
[Fact]
public void GetResidualCost_Works()
{
// arrange
int ctx0 = 0;
int expected = 20911;
Vp8Residual residual = ReadVp8Residual(Path.Combine("TestDataWebp", "Vp8Residual.bin"));
// act
int actual = residual.GetResidualCost(ctx0);
// assert
Assert.Equal(expected, actual);
}
private static void CreateRandomProbas(Vp8EncProba probas, Random rand)
{
for (int t = 0; t < WebpConstants.NumTypes; ++t)
{
for (int b = 0; b < WebpConstants.NumBands; ++b)
{
for (int c = 0; c < WebpConstants.NumCtx; ++c)
{
for (int p = 0; p < WebpConstants.NumProbas; ++p)
{
probas.Coeffs[t][b].Probabilities[c].Probabilities[p] = (byte)rand.Next(255);
}
}
}
}
}
private static void CreateCosts(Vp8EncProba probas, Random rand)
{
for (int i = 0; i < probas.RemappedCosts.Length; i++)
{
for (int j = 0; j < probas.RemappedCosts[i].Length; j++)
{
for (int k = 0; k < probas.RemappedCosts[i][j].Costs.Length; k++)
{
ushort[] costs = probas.RemappedCosts[i][j].Costs[k].Costs;
for (int m = 0; m < costs.Length; m++)
{
costs[m] = (byte)rand.Next(255);
}
}
}
}
}
private static void RunSetCoeffsTest()
{
// arrange
var residual = new Vp8Residual();
Vp8Residual residual = new();
short[] coeffs = { 110, 0, -2, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0 };
// act
@ -23,11 +249,8 @@ public class Vp8ResidualTests
}
[Fact]
public void RunSetCoeffsTest_Works() => RunSetCoeffsTest();
[Fact]
public void RunSetCoeffsTest_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunSetCoeffsTest, HwIntrinsics.AllowAll);
public void SetCoeffsTest_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunSetCoeffsTest, HwIntrinsics.AllowAll);
[Fact]
public void RunSetCoeffsTest_WithoutHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunSetCoeffsTest, HwIntrinsics.DisableHWIntrinsic);
public void SetCoeffsTest_WithoutSSE2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunSetCoeffsTest, HwIntrinsics.DisableSSE2);
}

6
tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics.X86;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.PixelFormats;
@ -367,10 +368,11 @@ public class WebpDecoderTests
image.DebugSave(provider, testOutputDetails: details, appendPixelTypeToFileName: false);
// Floating point differences result in minor pixel differences.
// Floating point differences in FMA used in the ResizeKernel result in minor pixel differences.
// Output have been manually verified.
// For more details see discussion: https://github.com/SixLabors/ImageSharp/pull/1513#issuecomment-763643594
image.CompareToReferenceOutput(
ImageComparer.TolerantPercentage(TestEnvironment.OSArchitecture == Architecture.Arm64 ? 0.0156F : 0.0007F),
ImageComparer.TolerantPercentage(Fma.IsSupported ? 0.0007F : 0.0156F),
provider,
testOutputDetails: details,
appendPixelTypeToFileName: false);

39
tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs

@ -2,6 +2,8 @@
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
using Castle.Core.Configuration;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@ -406,6 +408,43 @@ public class ParallelRowIteratorTests
Assert.Contains(width <= 0 ? "Width" : "Height", ex.Message);
}
[Fact]
public void CanIterateWithoutIntOverflow()
{
ParallelExecutionSettings parallelSettings = ParallelExecutionSettings.FromConfiguration(Configuration.Default);
const int max = 100_000;
Rectangle rect = new(0, 0, max, max);
int intervalMaxY = 0;
void RowAction(RowInterval rows, Span<Rgba32> memory) => intervalMaxY = Math.Max(rows.Max, intervalMaxY);
TestRowOperation operation = new();
TestRowIntervalOperation<Rgba32> intervalOperation = new(RowAction);
ParallelRowIterator.IterateRows(Configuration.Default, rect, in operation);
Assert.Equal(max - 1, operation.MaxY.Value);
ParallelRowIterator.IterateRowIntervals<TestRowIntervalOperation<Rgba32>, Rgba32>(rect, in parallelSettings, in intervalOperation);
Assert.Equal(max, intervalMaxY);
}
private readonly struct TestRowOperation : IRowOperation
{
public TestRowOperation()
{
}
public StrongBox<int> MaxY { get; } = new StrongBox<int>();
public void Invoke(int y)
{
lock (this.MaxY)
{
this.MaxY.Value = Math.Max(y, this.MaxY.Value);
}
}
}
private readonly struct TestRowIntervalOperation : IRowIntervalOperation
{
private readonly Action<RowInterval> action;

109
tests/ImageSharp.Tests/IO/LocalFileSystemTests.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.IO;
@ -11,36 +11,113 @@ public class LocalFileSystemTests
public void OpenRead()
{
string path = Path.GetTempFileName();
string testData = Guid.NewGuid().ToString();
File.WriteAllText(path, testData);
try
{
string testData = Guid.NewGuid().ToString();
File.WriteAllText(path, testData);
var fs = new LocalFileSystem();
LocalFileSystem fs = new();
using (var r = new StreamReader(fs.OpenRead(path)))
{
string data = r.ReadToEnd();
using (FileStream stream = (FileStream)fs.OpenRead(path))
using (StreamReader reader = new(stream))
{
Assert.False(stream.IsAsync);
Assert.True(stream.CanRead);
Assert.False(stream.CanWrite);
Assert.Equal(testData, data);
string data = reader.ReadToEnd();
Assert.Equal(testData, data);
}
}
finally
{
File.Delete(path);
}
}
File.Delete(path);
[Fact]
public async Task OpenReadAsynchronous()
{
string path = Path.GetTempFileName();
try
{
string testData = Guid.NewGuid().ToString();
File.WriteAllText(path, testData);
LocalFileSystem fs = new();
await using (FileStream stream = (FileStream)fs.OpenReadAsynchronous(path))
using (StreamReader reader = new(stream))
{
Assert.True(stream.IsAsync);
Assert.True(stream.CanRead);
Assert.False(stream.CanWrite);
string data = await reader.ReadToEndAsync();
Assert.Equal(testData, data);
}
}
finally
{
File.Delete(path);
}
}
[Fact]
public void Create()
{
string path = Path.GetTempFileName();
string testData = Guid.NewGuid().ToString();
var fs = new LocalFileSystem();
try
{
string testData = Guid.NewGuid().ToString();
LocalFileSystem fs = new();
using (FileStream stream = (FileStream)fs.Create(path))
using (StreamWriter writer = new(stream))
{
Assert.False(stream.IsAsync);
Assert.True(stream.CanRead);
Assert.True(stream.CanWrite);
using (var r = new StreamWriter(fs.Create(path)))
writer.Write(testData);
}
string data = File.ReadAllText(path);
Assert.Equal(testData, data);
}
finally
{
r.Write(testData);
File.Delete(path);
}
}
string data = File.ReadAllText(path);
Assert.Equal(testData, data);
[Fact]
public async Task CreateAsynchronous()
{
string path = Path.GetTempFileName();
try
{
string testData = Guid.NewGuid().ToString();
LocalFileSystem fs = new();
await using (FileStream stream = (FileStream)fs.CreateAsynchronous(path))
await using (StreamWriter writer = new(stream))
{
Assert.True(stream.IsAsync);
Assert.True(stream.CanRead);
Assert.True(stream.CanWrite);
await writer.WriteAsync(testData);
}
File.Delete(path);
string data = File.ReadAllText(path);
Assert.Equal(testData, data);
}
finally
{
File.Delete(path);
}
}
}

8
tests/ImageSharp.Tests/Image/ImageSaveTests.cs

@ -44,7 +44,7 @@ public class ImageSaveTests : IDisposable
[Fact]
public void SavePath()
{
var stream = new MemoryStream();
using MemoryStream stream = new();
this.fileSystem.Setup(x => x.Create("path.png")).Returns(stream);
this.image.Save("path.png");
@ -54,7 +54,7 @@ public class ImageSaveTests : IDisposable
[Fact]
public void SavePathWithEncoder()
{
var stream = new MemoryStream();
using MemoryStream stream = new();
this.fileSystem.Setup(x => x.Create("path.jpg")).Returns(stream);
this.image.Save("path.jpg", this.encoderNotInFormat.Object);
@ -73,7 +73,7 @@ public class ImageSaveTests : IDisposable
[Fact]
public void SaveStreamWithMime()
{
var stream = new MemoryStream();
using MemoryStream stream = new();
this.image.Save(stream, this.localImageFormat.Object);
this.encoder.Verify(x => x.Encode(this.image, stream));
@ -82,7 +82,7 @@ public class ImageSaveTests : IDisposable
[Fact]
public void SaveStreamWithEncoder()
{
var stream = new MemoryStream();
using MemoryStream stream = new();
this.image.Save(stream, this.encoderNotInFormat.Object);

6
tests/ImageSharp.Tests/Image/ImageTests.ImageLoadTestBase.cs

@ -122,6 +122,7 @@ public partial class ImageTests
Stream StreamFactory() => this.DataStream;
this.LocalFileSystemMock.Setup(x => x.OpenRead(this.MockFilePath)).Returns(StreamFactory);
this.LocalFileSystemMock.Setup(x => x.OpenReadAsynchronous(this.MockFilePath)).Returns(StreamFactory);
this.topLevelFileSystem.AddFile(this.MockFilePath, StreamFactory);
this.LocalConfiguration.FileSystem = this.LocalFileSystemMock.Object;
this.TopLevelConfiguration.FileSystem = this.topLevelFileSystem;
@ -132,6 +133,11 @@ public partial class ImageTests
// Clean up the global object;
this.localStreamReturnImageRgba32?.Dispose();
this.localStreamReturnImageAgnostic?.Dispose();
if (this.dataStreamLazy.IsValueCreated)
{
this.dataStreamLazy.Value.Dispose();
}
}
protected virtual Stream CreateStream() => this.TestFormat.CreateStream(this.Marker);

7
tests/ImageSharp.Tests/ImageSharp.Tests.csproj

@ -56,6 +56,9 @@
<None Update="TestFonts\SixLaborsSampleAB.woff">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="TestDataWebp\Vp8Residual.bin">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="xunit.runner.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
@ -73,5 +76,9 @@
<Service Include="{508349b6-6b84-4df5-91f0-309beebad82d}" />
</ItemGroup>
<ItemGroup>
<Folder Include="Formats\Qoi\" />
</ItemGroup>
</Project>

8
tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedMemoryPoolTests.Trim.cs

@ -113,9 +113,15 @@ public partial class UniformUnmanagedMemoryPoolTests
public static readonly bool Is32BitProcess = !Environment.Is64BitProcess;
private static readonly List<byte[]> PressureArrays = new();
[ConditionalFact(nameof(Is32BitProcess))]
[Fact]
public static void GC_Collect_OnHighLoad_TrimsEntirePool()
{
if (!Is32BitProcess)
{
// This test is only relevant for 32-bit processes.
return;
}
RemoteExecutor.Invoke(RunTest).Dispose();
static void RunTest()

45
tests/ImageSharp.Tests/Numerics/RationalTests.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Tests;
@ -14,15 +14,15 @@ public class RationalTests
[Fact]
public void AreEqual()
{
var r1 = new Rational(3, 2);
var r2 = new Rational(3, 2);
Rational r1 = new(3, 2);
Rational r2 = new(3, 2);
Assert.Equal(r1, r2);
Assert.True(r1 == r2);
var r3 = new Rational(7.55);
var r4 = new Rational(755, 100);
var r5 = new Rational(151, 20);
Rational r3 = new(7.55);
Rational r4 = new(755, 100);
Rational r5 = new(151, 20);
Assert.Equal(r3, r4);
Assert.Equal(r4, r5);
@ -34,20 +34,39 @@ public class RationalTests
[Fact]
public void AreNotEqual()
{
var first = new Rational(0, 100);
var second = new Rational(100, 100);
Rational first = new(0, 100);
Rational second = new(100, 100);
Assert.NotEqual(first, second);
Assert.True(first != second);
}
/// <summary>
/// Tests known out-of-range values.
/// </summary>
/// <param name="value">The input value.</param>
/// <param name="numerator">The expected numerator.</param>
/// <param name="denominator">The expected denominator.</param>
[Theory]
[InlineData(0, 0, 1)]
[InlineData(double.NaN, 0, 0)]
[InlineData(double.PositiveInfinity, 1, 0)]
[InlineData(double.NegativeInfinity, 1, 0)]
public void FromDoubleOutOfRange(double value, uint numerator, uint denominator)
{
Rational r = Rational.FromDouble(value);
Assert.Equal(numerator, r.Numerator);
Assert.Equal(denominator, r.Denominator);
}
/// <summary>
/// Tests whether the Rational constructor correctly assign properties.
/// </summary>
[Fact]
public void ConstructorAssignsProperties()
{
var rational = new Rational(7, 55);
Rational rational = new(7, 55);
Assert.Equal(7U, rational.Numerator);
Assert.Equal(55U, rational.Denominator);
@ -71,15 +90,15 @@ public class RationalTests
[Fact]
public void Fraction()
{
var first = new Rational(1.0 / 1600);
var second = new Rational(1.0 / 1600, true);
Rational first = new(1.0 / 1600);
Rational second = new(1.0 / 1600, true);
Assert.False(first.Equals(second));
}
[Fact]
public void ToDouble()
{
var rational = new Rational(0, 0);
Rational rational = new(0, 0);
Assert.Equal(double.NaN, rational.ToDouble());
rational = new Rational(2, 0);
@ -89,7 +108,7 @@ public class RationalTests
[Fact]
public void ToStringRepresentation()
{
var rational = new Rational(0, 0);
Rational rational = new(0, 0);
Assert.Equal("[ Indeterminate ]", rational.ToString());
rational = new Rational(double.PositiveInfinity);

13
tests/ImageSharp.Tests/Processing/Processors/Effects/OilPaintTest.cs

@ -27,8 +27,7 @@ public class OilPaintTest
[WithFileCollection(nameof(InputImages), nameof(OilPaintValues), PixelTypes.Rgba32)]
public void FullImage<TPixel>(TestImageProvider<TPixel> provider, int levels, int brushSize)
where TPixel : unmanaged, IPixel<TPixel>
{
provider.RunValidatingProcessorTest(
=> provider.RunValidatingProcessorTest(
x =>
{
x.OilPaint(levels, brushSize);
@ -36,17 +35,21 @@ public class OilPaintTest
},
ImageComparer.TolerantPercentage(0.01F),
appendPixelTypeToFileName: false);
}
[Theory]
[WithFileCollection(nameof(InputImages), nameof(OilPaintValues), PixelTypes.Rgba32)]
[WithTestPatternImages(nameof(OilPaintValues), 100, 100, PixelTypes.Rgba32)]
public void InBox<TPixel>(TestImageProvider<TPixel> provider, int levels, int brushSize)
where TPixel : unmanaged, IPixel<TPixel>
{
provider.RunRectangleConstrainedValidatingProcessorTest(
=> provider.RunRectangleConstrainedValidatingProcessorTest(
(x, rect) => x.OilPaint(levels, brushSize, rect),
$"{levels}-{brushSize}",
ImageComparer.TolerantPercentage(0.01F));
[Fact]
public void Issue2518_PixelComponentOutsideOfRange_ThrowsImageProcessingException()
{
using Image<RgbaVector> image = new(10, 10, new RgbaVector(1, 1, 100));
Assert.Throws<ImageProcessingException>(() => image.Mutate(ctx => ctx.OilPaint()));
}
}

BIN
tests/ImageSharp.Tests/TestDataWebp/Vp8Residual.bin

Binary file not shown.

1
tests/ImageSharp.Tests/TestDataWebp/Vp8Residual.json

File diff suppressed because one or more lines are too long

44
tests/ImageSharp.Tests/TestFileSystem.cs

@ -1,6 +1,8 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
#nullable enable
namespace SixLabors.ImageSharp.Tests;
/// <summary>
@ -8,7 +10,7 @@ namespace SixLabors.ImageSharp.Tests;
/// </summary>
public class TestFileSystem : ImageSharp.IO.IFileSystem
{
private readonly Dictionary<string, Func<Stream>> fileSystem = new Dictionary<string, Func<Stream>>(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, Func<Stream>> fileSystem = new(StringComparer.OrdinalIgnoreCase);
public void AddFile(string path, Func<Stream> data)
{
@ -18,35 +20,39 @@ public class TestFileSystem : ImageSharp.IO.IFileSystem
}
}
public Stream Create(string path)
public Stream Create(string path) => this.GetStream(path) ?? File.Create(path);
public Stream CreateAsynchronous(string path) => this.GetStream(path) ?? File.Open(path, new FileStreamOptions
{
// if we have injected a fake file use it instead
lock (this.fileSystem)
{
if (this.fileSystem.ContainsKey(path))
{
Stream stream = this.fileSystem[path]();
stream.Position = 0;
return stream;
}
}
Mode = FileMode.Create,
Access = FileAccess.ReadWrite,
Share = FileShare.None,
Options = FileOptions.Asynchronous,
});
return File.Create(path);
}
public Stream OpenRead(string path) => this.GetStream(path) ?? File.OpenRead(path);
public Stream OpenReadAsynchronous(string path) => this.GetStream(path) ?? File.Open(path, new FileStreamOptions
{
Mode = FileMode.Open,
Access = FileAccess.Read,
Share = FileShare.Read,
Options = FileOptions.Asynchronous,
});
public Stream OpenRead(string path)
private Stream? GetStream(string path)
{
// if we have injected a fake file use it instead
lock (this.fileSystem)
{
if (this.fileSystem.ContainsKey(path))
if (this.fileSystem.TryGetValue(path, out Func<Stream>? streamFactory))
{
Stream stream = this.fileSystem[path]();
Stream stream = streamFactory();
stream.Position = 0;
return stream;
}
}
return File.OpenRead(path);
return null;
}
}

27
tests/ImageSharp.Tests/TestImages.cs

@ -129,9 +129,15 @@ public static class TestImages
// Issue 2209: https://github.com/SixLabors/ImageSharp/issues/2209
public const string Issue2209IndexedWithTransparency = "Png/issues/Issue_2209.png";
// Issue 2259: https://github.com/SixLabors/ImageSharp/issues/2259
// Issue 2259: https://github.com/SixLabors/ImageSharp/issues/2469
public const string Issue2259 = "Png/issues/Issue_2259.png";
// Issue 2259: https://github.com/SixLabors/ImageSharp/issues/2469
public const string Issue2469 = "Png/issues/issue_2469.png";
// Issue 2447: https://github.com/SixLabors/ImageSharp/issues/2447
public const string Issue2447 = "Png/issues/issue_2447.png";
public static class Bad
{
public const string MissingDataChunk = "Png/xdtn0g01.png";
@ -295,6 +301,8 @@ public static class TestImages
public const string Issue2315_NotEnoughBytes = "Jpg/issues/issue-2315.jpg";
public const string Issue2334_NotEnoughBytesA = "Jpg/issues/issue-2334-a.jpg";
public const string Issue2334_NotEnoughBytesB = "Jpg/issues/issue-2334-b.jpg";
public const string Issue2478_JFXX = "Jpg/issues/issue-2478-jfxx.jpg";
public const string HangBadScan = "Jpg/issues/Hang_C438A851.jpg";
public static class Fuzz
{
@ -418,6 +426,8 @@ public static class TestImages
public const string Rgba321010102 = "Bmp/rgba32-1010102.bmp";
public const string RgbaAlphaBitfields = "Bmp/rgba32abf.bmp";
public const string BlackWhitePalletDataMatrix = "Bmp/bit1datamatrix.bmp";
public static readonly string[] BitFields =
{
Rgb32bfdef,
@ -969,12 +979,14 @@ public static class TestImages
public const string Cmyk = "Tiff/Cmyk.tiff";
public const string Cmyk64BitDeflate = "Tiff/cmyk_deflate_64bit.tiff";
public const string CmykLzwPredictor = "Tiff/Cmyk-lzw-predictor.tiff";
public const string Issues1716Rgb161616BitLittleEndian = "Tiff/Issues/Issue1716.tiff";
public const string Issues1891 = "Tiff/Issues/Issue1891.tiff";
public const string Issues2123 = "Tiff/Issues/Issue2123.tiff";
public const string Issues2149 = "Tiff/Issues/Group4CompressionWithStrips.tiff";
public const string Issues2255 = "Tiff/Issues/Issue2255.png";
public const string Issues2435 = "Tiff/Issues/Issue2435.tiff";
public const string SmallRgbDeflate = "Tiff/rgb_small_deflate.tiff";
public const string SmallRgbLzw = "Tiff/rgb_small_lzw.tiff";
@ -1039,5 +1051,18 @@ public static class TestImages
public const string RgbPlain = "Pbm/rgb_plain.ppm";
public const string RgbPlainNormalized = "Pbm/rgb_plain_normalized.ppm";
public const string RgbPlainMagick = "Pbm/rgb_plain_magick.ppm";
public const string Issue2477 = "Pbm/issue2477.pbm";
}
public static class Qoi
{
public const string Dice = "Qoi/dice.qoi";
public const string EdgeCase = "Qoi/edgecase.qoi";
public const string Kodim10 = "Qoi/kodim10.qoi";
public const string Kodim23 = "Qoi/kodim23.qoi";
public const string QoiLogo = "Qoi/qoi_logo.qoi";
public const string TestCard = "Qoi/testcard.qoi";
public const string TestCardRGBA = "Qoi/testcard_rgba.qoi";
public const string Wikipedia008 = "Qoi/wikipedia_008.qoi";
}
}

4
tests/ImageSharp.Tests/TestUtilities/SingleStreamFileSystem.cs

@ -13,5 +13,9 @@ internal class SingleStreamFileSystem : IFileSystem
Stream IFileSystem.Create(string path) => this.stream;
Stream IFileSystem.CreateAsynchronous(string path) => this.stream;
Stream IFileSystem.OpenRead(string path) => this.stream;
Stream IFileSystem.OpenReadAsynchronous(string path) => this.stream;
}

4
tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs

@ -8,6 +8,7 @@ using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Pbm;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Qoi;
using SixLabors.ImageSharp.Formats.Tga;
using SixLabors.ImageSharp.Formats.Tiff;
using SixLabors.ImageSharp.Formats.Webp;
@ -62,7 +63,8 @@ public static partial class TestEnvironment
new PbmConfigurationModule(),
new TgaConfigurationModule(),
new WebpConfigurationModule(),
new TiffConfigurationModule());
new TiffConfigurationModule(),
new QoiConfigurationModule());
IImageEncoder pngEncoder = IsWindows ? SystemDrawingReferenceEncoder.Png : new ImageSharpPngEncoderWithDefaultConfiguration();
IImageEncoder bmpEncoder = IsWindows ? SystemDrawingReferenceEncoder.Bmp : new BmpEncoder();

4
tests/Images/External/ReferenceOutput/DitherTests/ApplyDitherFilterInBox_Rgba32_CalliphoraPartial.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4d88eb2e50ca9dbed0e8dfe4ad278cd88ddb9d3408b30d9dfe59102b167f570b
size 262887
oid sha256:98115a7087aced0c28cefa32a57bc72be245886cabeefc4ff7faf7984236218c
size 271226

4
tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_Bike_Bayer16x16.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:97cfbef27319988b67aeac87d469d044edd925c90e4774170465f51eed85c16a
size 42915
oid sha256:58c03e354b108033873e2a4c0b043ce15919c4d0630e6ca72ff70b89cbedb979
size 44239

4
tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_Bike_Bayer2x2.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3a799b69938507e3fd2a74ffa7c6c6ad6574acb25861a0a50cb8361520d468de
size 41809
oid sha256:f987f4d270568facefc11eee7f81dd156af56c26b69fe3a6d2d2e9818652befa
size 43116

4
tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_Bike_Bayer4x4.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9932db58eeb966cd293b1b7a375e9c1b17b6d09153c679ebf03d42a08d2ce9b3
size 43332
oid sha256:ebdad83936e50bbb00fd74b7dd7d2f5a480bb7347aa3d151e7827107cd279bac
size 44441

4
tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_Bike_Bayer8x8.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:67ebf42bc82483d1778254d95a376230437611dce91c80f8ecda608de56bffe7
size 43108
oid sha256:ccdf5937c30999e3b09071200de2e1db63b606ad9cbf6f7677a7499fb0b52963
size 44252

4
tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_Bike_Ordered3x3.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8d5cdda990ac146a7580f58cc2bcab72f903dde564a394de7df4cc37e6dcf2dd
size 43906
oid sha256:baf70b732646d7c6cec60cfbe569ec673418dfb2dd0b5937bccfb91d9821d586
size 45053

4
tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_CalliphoraPartial_Bayer16x16.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c11e6c197bd1c227ae8f4af7e8c232cfe75db6929ab12bddf5e6554fbaed3f01
size 50716
oid sha256:f6a1eae610ed730e4cec41693829929ba8db674886c2bd558f1b8893d2b76802
size 51201

4
tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_CalliphoraPartial_Bayer2x2.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:69ff9654eb61f2bfdd44fb25aff959c5b831015e283cc91a90e3abf6f681dc88
size 52429
oid sha256:ba674e0236c2e146c64a7f3e224c702030769304cd0fd624d1989536da341659
size 52814

4
tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_CalliphoraPartial_Bayer4x4.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6ee945ac5120e4198d1f94e6467cc0f77c90869bf5a09942e7720dddcfdfbe07
size 51262
oid sha256:316231c8d837f864cf62dcc79fdce698dc8c45c0327372de42c2b89eac1d9f81
size 51851

4
tests/Images/External/ReferenceOutput/DitherTests/DitherFilter_WorksWithAllDitherers_CalliphoraPartial_Bayer8x8.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1b023505175ae39a93fa55c85aa31466f0aca76fab0ee54f9667648b91f9aeb9
size 50789
oid sha256:b58144146585f50960dfd6ac5dc3f52238160287ae5f9b18c6796962cc3d2fd2
size 51550

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save