diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 0943ab4eb..ffacf51e4 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -28,7 +28,7 @@ #### **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/master/ReferenceOutput). To succesfully run tests, make sure that you have updated the submodules! +* 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?** diff --git a/.github/ISSUE_TEMPLATE/commercial-bug-report.md b/.github/ISSUE_TEMPLATE/commercial-bug-report.md deleted file mode 100644 index 024de8e19..000000000 --- a/.github/ISSUE_TEMPLATE/commercial-bug-report.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -name: "Commercial License : Bug Report" -about: | - 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 - ---- - - -### Prerequisites - -- [ ] I have written a descriptive issue title -- [ ] I have verified that I am running the latest version of ImageSharp -- [ ] I have verified if the problem exist in both `DEBUG` and `RELEASE` mode -- [ ] 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 - -### Description - - -### Steps to Reproduce - - -### System Configuration - - -- ImageSharp version: -- Other ImageSharp packages and versions: -- Environment (Operating system, version and so on): -- .NET Framework version: -- Additional information: - - diff --git a/.github/ISSUE_TEMPLATE/commercial-bug-report.yml b/.github/ISSUE_TEMPLATE/commercial-bug-report.yml new file mode 100644 index 000000000..6b4d914d7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/commercial-bug-report.yml @@ -0,0 +1,56 @@ +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. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 62a8bf2b4..ec9258883 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,5 @@ blank_issues_enabled: false contact_links: - name: Feature Request - url: https://github.com/SixLabors/ImageSharp/discussions?discussions_q=category%3AIdeas + url: https://github.com/SixLabors/ImageSharp/discussions/categories/ideas about: Share ideas for new features for this project. diff --git a/.github/ISSUE_TEMPLATE/oss-bug-report.md b/.github/ISSUE_TEMPLATE/oss-bug-report.md deleted file mode 100644 index 9e9567a99..000000000 --- a/.github/ISSUE_TEMPLATE/oss-bug-report.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -name: "OSS : Bug Report" -about: Create a report to help us improve the project. OSS Issues are not guaranteed to be triaged. -labels: needs triage - ---- - -### Prerequisites - -- [ ] I have written a descriptive issue title -- [ ] I have verified that I am running the latest version of ImageSharp -- [ ] I have verified if the problem exist in both `DEBUG` and `RELEASE` mode -- [ ] 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 - -### Description - - -### Steps to Reproduce - - -### System Configuration - - -- ImageSharp version: -- Other ImageSharp packages and versions: -- Environment (Operating system, version and so on): -- .NET Framework version: -- Additional information: - - diff --git a/.github/ISSUE_TEMPLATE/oss-bug-report.yml b/.github/ISSUE_TEMPLATE/oss-bug-report.yml new file mode 100644 index 000000000..a4e5619d4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/oss-bug-report.yml @@ -0,0 +1,52 @@ +name: "OSS : Bug Report" +description: Create a report to help us improve the project. OSS Issues are not guaranteed to be triaged. +labels: ["needs triage"] +body: +- type: checkboxes + attributes: + label: Prerequisites + options: + - 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. diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 6d31e8c53..16f8ebb06 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -3,71 +3,50 @@ name: Build on: push: branches: - - master + - main tags: - "v*" pull_request: branches: - - master + - main jobs: Build: strategy: matrix: options: - os: ubuntu-latest - framework: net6.0 - sdk: 6.0.x + framework: net7.0 + sdk: 7.0.x sdk-preview: true runtime: -x64 codecov: false - os: macos-latest - framework: net6.0 - sdk: 6.0.x + framework: net7.0 + sdk: 7.0.x sdk-preview: true runtime: -x64 codecov: false - os: windows-latest - framework: net6.0 - sdk: 6.0.x + framework: net7.0 + sdk: 7.0.x sdk-preview: true runtime: -x64 codecov: false - os: ubuntu-latest - framework: net5.0 - runtime: -x64 - codecov: false - - os: macos-latest - framework: net5.0 - runtime: -x64 - codecov: false - - os: windows-latest - framework: net5.0 - runtime: -x64 - codecov: false - - os: ubuntu-latest - framework: netcoreapp3.1 + framework: net6.0 + sdk: 6.0.x runtime: -x64 codecov: false - os: macos-latest - framework: netcoreapp3.1 - runtime: -x64 - codecov: false - - os: windows-latest - framework: netcoreapp3.1 - runtime: -x64 - codecov: false - - os: windows-latest - framework: netcoreapp2.1 + framework: net6.0 + sdk: 6.0.x runtime: -x64 codecov: false - os: windows-latest - framework: net472 + framework: net6.0 + sdk: 6.0.x runtime: -x64 codecov: false - - os: windows-latest - framework: net472 - runtime: -x86 - codecov: false runs-on: ${{matrix.options.os}} @@ -109,12 +88,13 @@ jobs: key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/*.props', '**/*.targets') }} restore-keys: ${{ runner.os }}-nuget- - - name: DotNet Setup Preview - if: ${{ matrix.options.sdk-preview == true }} + - name: DotNet Setup uses: actions/setup-dotnet@v1 with: - dotnet-version: ${{ matrix.options.sdk }} include-prerelease: true + dotnet-version: | + 7.0.x + 6.0.x - name: DotNet Build if: ${{ matrix.options.sdk-preview != true }} diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 2b14f2a4b..3f8a82031 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -10,7 +10,7 @@ jobs: matrix: options: - os: ubuntu-latest - framework: netcoreapp3.1 + framework: net6.0 runtime: -x64 codecov: true @@ -54,6 +54,12 @@ jobs: key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/*.props', '**/*.targets') }} restore-keys: ${{ runner.os }}-nuget- + - name: DotNet Setup + uses: actions/setup-dotnet@v1 + with: + dotnet-version: | + 6.0.x + - name: DotNet Build shell: pwsh run: ./ci-build.ps1 "${{matrix.options.framework}}" diff --git a/ImageSharp.sln b/ImageSharp.sln index 17d293b43..fbf1ca24b 100644 --- a/ImageSharp.sln +++ b/ImageSharp.sln @@ -142,6 +142,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Gif", "Gif", "{EE3FB0B3-1C3 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "issues", "issues", "{BF8DFDC1-CEE5-4A37-B216-D3085360C776}" ProjectSection(SolutionItems) = preProject + tests\Images\Input\Gif\issues\bugzilla-55918.gif = tests\Images\Input\Gif\issues\bugzilla-55918.gif + tests\Images\Input\Gif\issues\issue1505_argumentoutofrange.png = tests\Images\Input\Gif\issues\issue1505_argumentoutofrange.png + tests\Images\Input\Gif\issues\issue1530.gif = tests\Images\Input\Gif\issues\issue1530.gif + tests\Images\Input\Gif\issues\issue1668_invalidcolorindex.gif = tests\Images\Input\Gif\issues\issue1668_invalidcolorindex.gif + tests\Images\Input\Gif\issues\issue1962_tiniest_gif_1st.gif = tests\Images\Input\Gif\issues\issue1962_tiniest_gif_1st.gif + tests\Images\Input\Gif\issues\issue2012_drona1.gif = tests\Images\Input\Gif\issues\issue2012_drona1.gif + tests\Images\Input\Gif\issues\issue2012_Stronghold-Crusader-Extreme-Cover.gif = tests\Images\Input\Gif\issues\issue2012_Stronghold-Crusader-Extreme-Cover.gif tests\Images\Input\Gif\issues\issue403_baddescriptorwidth.gif = tests\Images\Input\Gif\issues\issue403_baddescriptorwidth.gif tests\Images\Input\Gif\issues\issue405_badappextlength252-2.gif = tests\Images\Input\Gif\issues\issue405_badappextlength252-2.gif tests\Images\Input\Gif\issues\issue405_badappextlength252.gif = tests\Images\Input\Gif\issues\issue405_badappextlength252.gif @@ -647,43 +654,25 @@ Global EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU - Debug-InnerLoop|Any CPU = Debug-InnerLoop|Any CPU Release|Any CPU = Release|Any CPU - Release-InnerLoop|Any CPU = Release-InnerLoop|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {2AA31A1F-142C-43F4-8687-09ABCA4B3A26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2AA31A1F-142C-43F4-8687-09ABCA4B3A26}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2AA31A1F-142C-43F4-8687-09ABCA4B3A26}.Debug-InnerLoop|Any CPU.ActiveCfg = Debug-InnerLoop|Any CPU - {2AA31A1F-142C-43F4-8687-09ABCA4B3A26}.Debug-InnerLoop|Any CPU.Build.0 = Debug-InnerLoop|Any CPU {2AA31A1F-142C-43F4-8687-09ABCA4B3A26}.Release|Any CPU.ActiveCfg = Release|Any CPU {2AA31A1F-142C-43F4-8687-09ABCA4B3A26}.Release|Any CPU.Build.0 = Release|Any CPU - {2AA31A1F-142C-43F4-8687-09ABCA4B3A26}.Release-InnerLoop|Any CPU.ActiveCfg = Release-InnerLoop|Any CPU - {2AA31A1F-142C-43F4-8687-09ABCA4B3A26}.Release-InnerLoop|Any CPU.Build.0 = Release-InnerLoop|Any CPU {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug-InnerLoop|Any CPU.ActiveCfg = Debug-InnerLoop|Any CPU - {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug-InnerLoop|Any CPU.Build.0 = Debug-InnerLoop|Any CPU {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release|Any CPU.ActiveCfg = Release|Any CPU {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release|Any CPU.Build.0 = Release|Any CPU - {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release-InnerLoop|Any CPU.ActiveCfg = Release-InnerLoop|Any CPU - {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release-InnerLoop|Any CPU.Build.0 = Release-InnerLoop|Any CPU {2BF743D8-2A06-412D-96D7-F448F00C5EA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2BF743D8-2A06-412D-96D7-F448F00C5EA5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2BF743D8-2A06-412D-96D7-F448F00C5EA5}.Debug-InnerLoop|Any CPU.ActiveCfg = Debug-InnerLoop|Any CPU - {2BF743D8-2A06-412D-96D7-F448F00C5EA5}.Debug-InnerLoop|Any CPU.Build.0 = Debug-InnerLoop|Any CPU {2BF743D8-2A06-412D-96D7-F448F00C5EA5}.Release|Any CPU.ActiveCfg = Release|Any CPU {2BF743D8-2A06-412D-96D7-F448F00C5EA5}.Release|Any CPU.Build.0 = Release|Any CPU - {2BF743D8-2A06-412D-96D7-F448F00C5EA5}.Release-InnerLoop|Any CPU.ActiveCfg = Release-InnerLoop|Any CPU - {2BF743D8-2A06-412D-96D7-F448F00C5EA5}.Release-InnerLoop|Any CPU.Build.0 = Release-InnerLoop|Any CPU {FC527290-2F22-432C-B77B-6E815726B02C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FC527290-2F22-432C-B77B-6E815726B02C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FC527290-2F22-432C-B77B-6E815726B02C}.Debug-InnerLoop|Any CPU.ActiveCfg = Debug-InnerLoop|Any CPU - {FC527290-2F22-432C-B77B-6E815726B02C}.Debug-InnerLoop|Any CPU.Build.0 = Debug-InnerLoop|Any CPU {FC527290-2F22-432C-B77B-6E815726B02C}.Release|Any CPU.ActiveCfg = Release|Any CPU {FC527290-2F22-432C-B77B-6E815726B02C}.Release|Any CPU.Build.0 = Release|Any CPU - {FC527290-2F22-432C-B77B-6E815726B02C}.Release-InnerLoop|Any CPU.ActiveCfg = Release-InnerLoop|Any CPU - {FC527290-2F22-432C-B77B-6E815726B02C}.Release-InnerLoop|Any CPU.Build.0 = Release-InnerLoop|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/README.md b/README.md index fdf14b496..6c669fb78 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@

-SixLabors.ImageSharp +SixLabors.ImageSharp
SixLabors.ImageSharp

-[![Build Status](https://img.shields.io/github/workflow/status/SixLabors/ImageSharp/Build/master)](https://github.com/SixLabors/ImageSharp/actions) -[![Code coverage](https://codecov.io/gh/SixLabors/ImageSharp/branch/master/graph/badge.svg)](https://codecov.io/gh/SixLabors/ImageSharp) +[![Build Status](https://img.shields.io/github/workflow/status/SixLabors/ImageSharp/Build/main)](https://github.com/SixLabors/ImageSharp/actions) +[![Code coverage](https://codecov.io/gh/SixLabors/ImageSharp/branch/main/graph/badge.svg)](https://codecov.io/gh/SixLabors/ImageSharp) [![License: Apache 2.0](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![Twitter](https://img.shields.io/twitter/url/http/shields.io.svg?style=flat&logo=twitter)](https://twitter.com/intent/tweet?hashtags=imagesharp,dotnet,oss&text=ImageSharp.+A+new+cross-platform+2D+graphics+API+in+C%23&url=https%3a%2f%2fgithub.com%2fSixLabors%2fImageSharp&via=sixlabors) @@ -26,7 +26,7 @@ Built against [.NET Standard 2.0](https://docs.microsoft.com/en-us/dotnet/standa ## License - ImageSharp is licensed under the [Apache License, Version 2.0](https://opensource.org/licenses/Apache-2.0) -- An alternative Commercial Support License can be purchased **for projects and applications requiring support**. +- An alternative Six Labors License can be purchased **for projects and applications requiring developer support**. Please visit https://sixlabors.com/pricing for details. ## Support Six Labors @@ -39,12 +39,13 @@ Support the efforts of the development of the Six Labors projects. ## Documentation - [Detailed documentation](https://sixlabors.github.io/docs/) for the ImageSharp API is available. This includes additional conceptual documentation to help you get started. -- Our [Samples Repository](https://github.com/SixLabors/Samples/tree/master/ImageSharp) is also available containing buildable code samples demonstrating common activities. +- Our [Samples Repository](https://github.com/SixLabors/Samples/tree/main/ImageSharp) is also available containing buildable code samples demonstrating common activities. ## Questions -- Do you have questions? We are happy to help! Please [join our Discussions Forum](https://github.com/SixLabors/ImageSharp/discussions/category_choices), or ask them on [Stack Overflow](https://stackoverflow.com) using the `ImageSharp` tag. Please do not open issues for questions. -- Please read our [Contribution Guide](https://github.com/SixLabors/ImageSharp/blob/master/.github/CONTRIBUTING.md) before opening issues or pull requests! +- Do you have questions? We are happy to help! Simply purchase a [Six Labors License](https://sixlabors.com/pricing) for developer support. Please do not open issues for questions or misuse our [Discussions Forum](https://github.com/SixLabors/ImageSharp/discussions). +- For feature ideas please [join our Discussions Forum](https://github.com/SixLabors/ImageSharp/discussions/categories/ideas) and we'll be happy to discuss. +- Please read our [Contribution Guide](https://github.com/SixLabors/ImageSharp/blob/main/.github/CONTRIBUTING.md) before opening issues or pull requests! ## 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. @@ -93,7 +94,7 @@ git submodule update --init --recursive ## How can you help? -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/master/.github/CONTRIBUTING.md) before opening a PR. +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. ## The ImageSharp Team diff --git a/src/Directory.Build.props b/src/Directory.Build.props index d211992a9..faa29865f 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -27,6 +27,7 @@ + diff --git a/src/ImageSharp/Advanced/AotCompilerTools.cs b/src/ImageSharp/Advanced/AotCompilerTools.cs index 82a146dc7..2323b5ba7 100644 --- a/src/ImageSharp/Advanced/AotCompilerTools.cs +++ b/src/ImageSharp/Advanced/AotCompilerTools.cs @@ -287,8 +287,7 @@ namespace SixLabors.ImageSharp.Advanced where TPixel : unmanaged, IPixel where TDecoder : class, IImageDecoder { - default(TDecoder).Decode(default, default); - default(TDecoder).DecodeAsync(default, default, default); + default(TDecoder).Decode(default, default, default); } /// diff --git a/src/ImageSharp/Common/Extensions/StreamExtensions.cs b/src/ImageSharp/Common/Extensions/StreamExtensions.cs index 1193eccee..8746989b3 100644 --- a/src/ImageSharp/Common/Extensions/StreamExtensions.cs +++ b/src/ImageSharp/Common/Extensions/StreamExtensions.cs @@ -13,7 +13,7 @@ namespace SixLabors.ImageSharp internal static class StreamExtensions { /// - /// Writes data from a stream into the provided buffer. + /// Writes data from a stream from the provided buffer. /// /// The stream. /// The buffer. diff --git a/src/ImageSharp/Compression/Zlib/Adler32.cs b/src/ImageSharp/Compression/Zlib/Adler32.cs index 7eb3f4516..1f3cbbca6 100644 --- a/src/ImageSharp/Compression/Zlib/Adler32.cs +++ b/src/ImageSharp/Compression/Zlib/Adler32.cs @@ -3,6 +3,7 @@ using System; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; #if SUPPORTS_RUNTIME_INTRINSICS using System.Runtime.Intrinsics; using System.Runtime.Intrinsics.X86; @@ -31,6 +32,8 @@ namespace SixLabors.ImageSharp.Compression.Zlib #if SUPPORTS_RUNTIME_INTRINSICS private const int MinBufferSize = 64; + private const int BlockSize = 1 << 5; + // The C# compiler emits this as a compile-time constant embedded in the PE file. private static ReadOnlySpan Tap1Tap2 => new byte[] { @@ -63,6 +66,11 @@ namespace SixLabors.ImageSharp.Compression.Zlib } #if SUPPORTS_RUNTIME_INTRINSICS + if (Avx2.IsSupported && buffer.Length >= MinBufferSize) + { + return CalculateAvx2(adler, buffer); + } + if (Ssse3.IsSupported && buffer.Length >= MinBufferSize) { return CalculateSse(adler, buffer); @@ -83,19 +91,15 @@ namespace SixLabors.ImageSharp.Compression.Zlib uint s2 = (adler >> 16) & 0xFFFF; // Process the data in blocks. - const int BLOCK_SIZE = 1 << 5; - uint length = (uint)buffer.Length; - uint blocks = length / BLOCK_SIZE; - length -= blocks * BLOCK_SIZE; + uint blocks = length / BlockSize; + length -= blocks * BlockSize; - int index = 0; - fixed (byte* bufferPtr = buffer) + fixed (byte* bufferPtr = &MemoryMarshal.GetReference(buffer)) { - fixed (byte* tapPtr = Tap1Tap2) + fixed (byte* tapPtr = &MemoryMarshal.GetReference(Tap1Tap2)) { - index += (int)blocks * BLOCK_SIZE; - var localBufferPtr = bufferPtr; + byte* localBufferPtr = bufferPtr; // _mm_setr_epi8 on x86 Vector128 tap1 = Sse2.LoadVector128((sbyte*)tapPtr); @@ -105,7 +109,7 @@ namespace SixLabors.ImageSharp.Compression.Zlib while (blocks > 0) { - uint n = NMAX / BLOCK_SIZE; /* The NMAX constraint. */ + uint n = NMAX / BlockSize; /* The NMAX constraint. */ if (n > blocks) { n = blocks; @@ -138,7 +142,7 @@ namespace SixLabors.ImageSharp.Compression.Zlib Vector128 mad2 = Ssse3.MultiplyAddAdjacent(bytes2, tap2); v_s2 = Sse2.Add(v_s2, Sse2.MultiplyAddAdjacent(mad2, ones).AsUInt32()); - localBufferPtr += BLOCK_SIZE; + localBufferPtr += BlockSize; } while (--n > 0); @@ -164,45 +168,127 @@ namespace SixLabors.ImageSharp.Compression.Zlib if (length > 0) { - if (length >= 16) - { - s2 += s1 += localBufferPtr[0]; - s2 += s1 += localBufferPtr[1]; - s2 += s1 += localBufferPtr[2]; - s2 += s1 += localBufferPtr[3]; - s2 += s1 += localBufferPtr[4]; - s2 += s1 += localBufferPtr[5]; - s2 += s1 += localBufferPtr[6]; - s2 += s1 += localBufferPtr[7]; - s2 += s1 += localBufferPtr[8]; - s2 += s1 += localBufferPtr[9]; - s2 += s1 += localBufferPtr[10]; - s2 += s1 += localBufferPtr[11]; - s2 += s1 += localBufferPtr[12]; - s2 += s1 += localBufferPtr[13]; - s2 += s1 += localBufferPtr[14]; - s2 += s1 += localBufferPtr[15]; - - localBufferPtr += 16; - length -= 16; - } + HandleLeftOver(localBufferPtr, length, ref s1, ref s2); + } - while (length-- > 0) - { - s2 += s1 += *localBufferPtr++; - } + return s1 | (s2 << 16); + } + } + } - if (s1 >= BASE) - { - s1 -= BASE; - } + // Based on: https://github.com/zlib-ng/zlib-ng/blob/develop/arch/x86/adler32_avx2.c + [MethodImpl(InliningOptions.HotPath | InliningOptions.ShortMethod)] + public static unsafe uint CalculateAvx2(uint adler, ReadOnlySpan buffer) + { + uint s1 = adler & 0xFFFF; + uint s2 = (adler >> 16) & 0xFFFF; + uint length = (uint)buffer.Length; - s2 %= BASE; + fixed (byte* bufferPtr = &MemoryMarshal.GetReference(buffer)) + { + byte* localBufferPtr = bufferPtr; + + Vector256 zero = Vector256.Zero; + var dot3v = Vector256.Create((short)1); + var dot2v = Vector256.Create(32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1); + + // Process n blocks of data. At most NMAX data bytes can be + // processed before s2 must be reduced modulo BASE. + var vs1 = Vector256.CreateScalar(s1); + var vs2 = Vector256.CreateScalar(s2); + + while (length >= 32) + { + int k = length < NMAX ? (int)length : (int)NMAX; + k -= k % 32; + length -= (uint)k; + + Vector256 vs10 = vs1; + Vector256 vs3 = Vector256.Zero; + + while (k >= 32) + { + // Load 32 input bytes. + Vector256 block = Avx.LoadVector256(localBufferPtr); + + // Sum of abs diff, resulting in 2 x int32's + Vector256 vs1sad = Avx2.SumAbsoluteDifferences(block, zero); + + vs1 = Avx2.Add(vs1, vs1sad.AsUInt32()); + vs3 = Avx2.Add(vs3, vs10); + + // sum 32 uint8s to 16 shorts. + Vector256 vshortsum2 = Avx2.MultiplyAddAdjacent(block, dot2v); + + // sum 16 shorts to 8 uint32s. + Vector256 vsum2 = Avx2.MultiplyAddAdjacent(vshortsum2, dot3v); + + vs2 = Avx2.Add(vsum2.AsUInt32(), vs2); + vs10 = vs1; + + localBufferPtr += BlockSize; + k -= 32; } - return s1 | (s2 << 16); + // Defer the multiplication with 32 to outside of the loop. + vs3 = Avx2.ShiftLeftLogical(vs3, 5); + vs2 = Avx2.Add(vs2, vs3); + + s1 = (uint)Numerics.EvenReduceSum(vs1.AsInt32()); + s2 = (uint)Numerics.ReduceSum(vs2.AsInt32()); + + s1 %= BASE; + s2 %= BASE; + + vs1 = Vector256.CreateScalar(s1); + vs2 = Vector256.CreateScalar(s2); } + + if (length > 0) + { + HandleLeftOver(localBufferPtr, length, ref s1, ref s2); + } + + return s1 | (s2 << 16); + } + } + + private static unsafe void HandleLeftOver(byte* localBufferPtr, uint length, ref uint s1, ref uint s2) + { + if (length >= 16) + { + s2 += s1 += localBufferPtr[0]; + s2 += s1 += localBufferPtr[1]; + s2 += s1 += localBufferPtr[2]; + s2 += s1 += localBufferPtr[3]; + s2 += s1 += localBufferPtr[4]; + s2 += s1 += localBufferPtr[5]; + s2 += s1 += localBufferPtr[6]; + s2 += s1 += localBufferPtr[7]; + s2 += s1 += localBufferPtr[8]; + s2 += s1 += localBufferPtr[9]; + s2 += s1 += localBufferPtr[10]; + s2 += s1 += localBufferPtr[11]; + s2 += s1 += localBufferPtr[12]; + s2 += s1 += localBufferPtr[13]; + s2 += s1 += localBufferPtr[14]; + s2 += s1 += localBufferPtr[15]; + + localBufferPtr += 16; + length -= 16; } + + while (length-- > 0) + { + s2 += s1 += *localBufferPtr++; + } + + if (s1 >= BASE) + { + s1 -= BASE; + } + + s2 %= BASE; } #endif diff --git a/src/ImageSharp/Compression/Zlib/DeflaterEngine.cs b/src/ImageSharp/Compression/Zlib/DeflaterEngine.cs index 506b0f2c1..02fa5bf58 100644 --- a/src/ImageSharp/Compression/Zlib/DeflaterEngine.cs +++ b/src/ImageSharp/Compression/Zlib/DeflaterEngine.cs @@ -483,18 +483,21 @@ namespace SixLabors.ImageSharp.Compression.Zlib int niceLength = Math.Min(this.niceLength, this.lookahead); int matchStrt = this.matchStart; - this.matchLen = Math.Max(this.matchLen, DeflaterConstants.MIN_MATCH - 1); int matchLength = this.matchLen; + matchLength = Math.Max(matchLength, DeflaterConstants.MIN_MATCH - 1); + this.matchLen = matchLength; - if (scan + matchLength > scanMax) + if (scan > scanMax - matchLength) { return false; } + int scanEndPosition = scan + matchLength; + byte* pinnedWindow = this.pinnedWindowPointer; int scanStart = this.strstart; - byte scanEnd1 = pinnedWindow[scan + matchLength - 1]; - byte scanEnd = pinnedWindow[scan + matchLength]; + byte scanEnd1 = pinnedWindow[scanEndPosition - 1]; + byte scanEnd = pinnedWindow[scanEndPosition]; // Do not waste too much time if we already have a good match: if (matchLength >= this.goodLength) @@ -508,8 +511,9 @@ namespace SixLabors.ImageSharp.Compression.Zlib match = curMatch; scan = scanStart; - if (pinnedWindow[match + matchLength] != scanEnd - || pinnedWindow[match + matchLength - 1] != scanEnd1 + int matchEndPosition = match + matchLength; + if (pinnedWindow[matchEndPosition] != scanEnd + || pinnedWindow[matchEndPosition - 1] != scanEnd1 || pinnedWindow[match] != pinnedWindow[scan] || pinnedWindow[++match] != pinnedWindow[++scan]) { @@ -685,6 +689,7 @@ namespace SixLabors.ImageSharp.Compression.Zlib return false; } + const int windowLen = (2 * DeflaterConstants.WSIZE) - DeflaterConstants.MIN_LOOKAHEAD; while (this.lookahead >= DeflaterConstants.MIN_LOOKAHEAD || flush) { if (this.lookahead == 0) @@ -695,7 +700,7 @@ namespace SixLabors.ImageSharp.Compression.Zlib return false; } - if (this.strstart > (2 * DeflaterConstants.WSIZE) - DeflaterConstants.MIN_LOOKAHEAD) + if (this.strstart > windowLen) { // slide window, as FindLongestMatch needs this. // This should only happen when flushing and the window @@ -766,6 +771,7 @@ namespace SixLabors.ImageSharp.Compression.Zlib return false; } + const int windowLen = (2 * DeflaterConstants.WSIZE) - DeflaterConstants.MIN_LOOKAHEAD; while (this.lookahead >= DeflaterConstants.MIN_LOOKAHEAD || flush) { if (this.lookahead == 0) @@ -783,7 +789,7 @@ namespace SixLabors.ImageSharp.Compression.Zlib return false; } - if (this.strstart >= (2 * DeflaterConstants.WSIZE) - DeflaterConstants.MIN_LOOKAHEAD) + if (this.strstart >= windowLen) { // slide window, as FindLongestMatch needs this. // This should only happen when flushing and the window diff --git a/src/ImageSharp/Diagnostics/MemoryDiagnostics.cs b/src/ImageSharp/Diagnostics/MemoryDiagnostics.cs new file mode 100644 index 000000000..89f18cff6 --- /dev/null +++ b/src/ImageSharp/Diagnostics/MemoryDiagnostics.cs @@ -0,0 +1,100 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Threading; + +namespace SixLabors.ImageSharp.Diagnostics +{ + /// + /// Represents the method to handle . + /// + public delegate void UndisposedAllocationDelegate(string allocationStackTrace); + + /// + /// Utilities to track memory usage and detect memory leaks from not disposing ImageSharp objects. + /// + public static class MemoryDiagnostics + { + private static int totalUndisposedAllocationCount; + + private static UndisposedAllocationDelegate undisposedAllocation; + private static int undisposedAllocationSubscriptionCounter; + private static readonly object SyncRoot = new(); + + /// + /// Fires when an ImageSharp object's undisposed memory resource leaks to the finalizer. + /// The event brings significant overhead, and is intended to be used for troubleshooting only. + /// For production diagnostics, use . + /// + public static event UndisposedAllocationDelegate UndisposedAllocation + { + add + { + lock (SyncRoot) + { + undisposedAllocationSubscriptionCounter++; + undisposedAllocation += value; + } + } + + remove + { + lock (SyncRoot) + { + undisposedAllocation -= value; + undisposedAllocationSubscriptionCounter--; + } + } + } + + /// + /// Fires when ImageSharp allocates memory from a MemoryAllocator + /// + internal static event Action MemoryAllocated; + + /// + /// Fires when ImageSharp releases memory allocated from a MemoryAllocator + /// + internal static event Action MemoryReleased; + + /// + /// Gets a value indicating the total number of memory resource objects leaked to the finalizer. + /// + public static int TotalUndisposedAllocationCount => totalUndisposedAllocationCount; + + internal static bool UndisposedAllocationSubscribed => Volatile.Read(ref undisposedAllocationSubscriptionCounter) > 0; + + internal static void IncrementTotalUndisposedAllocationCount() + { + Interlocked.Increment(ref totalUndisposedAllocationCount); + MemoryAllocated?.Invoke(); + } + + internal static void DecrementTotalUndisposedAllocationCount() + { + Interlocked.Decrement(ref totalUndisposedAllocationCount); + MemoryReleased?.Invoke(); + } + + internal static void RaiseUndisposedMemoryResource(string allocationStackTrace) + { + if (undisposedAllocation is null) + { + return; + } + + // Schedule on the ThreadPool, to avoid user callback messing up the finalizer thread. +#if NETSTANDARD2_1 || NETCOREAPP2_1_OR_GREATER + ThreadPool.QueueUserWorkItem( + stackTrace => undisposedAllocation?.Invoke(stackTrace), + allocationStackTrace, + preferLocal: false); +#else + ThreadPool.QueueUserWorkItem( + stackTrace => undisposedAllocation?.Invoke((string)stackTrace), + allocationStackTrace); +#endif + } + } +} diff --git a/src/ImageSharp/Formats/Bmp/BmpColorSpace.cs b/src/ImageSharp/Formats/Bmp/BmpColorSpace.cs new file mode 100644 index 000000000..864087121 --- /dev/null +++ b/src/ImageSharp/Formats/Bmp/BmpColorSpace.cs @@ -0,0 +1,37 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +// ReSharper disable InconsistentNaming +namespace SixLabors.ImageSharp.Formats.Bmp +{ + /// + /// Enum for the different color spaces. + /// + internal enum BmpColorSpace + { + /// + /// This value implies that endpoints and gamma values are given in the appropriate fields. + /// + LCS_CALIBRATED_RGB = 0, + + /// + /// The Windows default color space ('Win '). + /// + LCS_WINDOWS_COLOR_SPACE = 1466527264, + + /// + /// Specifies that the bitmap is in sRGB color space ('sRGB'). + /// + LCS_sRGB = 1934772034, + + /// + /// This value indicates that bV5ProfileData points to the file name of the profile to use (gamma and endpoints values are ignored). + /// + PROFILE_LINKED = 1279872587, + + /// + /// This value indicates that bV5ProfileData points to a memory buffer that contains the profile to be used (gamma and endpoints values are ignored). + /// + PROFILE_EMBEDDED = 1296188740 + } +} diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoder.cs b/src/ImageSharp/Formats/Bmp/BmpDecoder.cs index 129b3a1aa..e76448938 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoder.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoder.cs @@ -3,9 +3,6 @@ using System.IO; using System.Threading; -using System.Threading.Tasks; -using SixLabors.ImageSharp.IO; -using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Bmp @@ -31,48 +28,25 @@ namespace SixLabors.ImageSharp.Formats.Bmp public RleSkippedPixelHandling RleSkippedPixelHandling { get; set; } = RleSkippedPixelHandling.Black; /// - public Image Decode(Configuration configuration, Stream stream) + public Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { Guard.NotNull(stream, nameof(stream)); var decoder = new BmpDecoderCore(configuration, this); - return decoder.Decode(configuration, stream); + return decoder.Decode(configuration, stream, cancellationToken); } /// - public Image Decode(Configuration configuration, Stream stream) - => this.Decode(configuration, stream); + public Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken) + => this.Decode(configuration, stream, cancellationToken); /// - public Task> DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel + public IImageInfo Identify(Configuration configuration, Stream stream, CancellationToken cancellationToken) { Guard.NotNull(stream, nameof(stream)); - var decoder = new BmpDecoderCore(configuration, this); - return decoder.DecodeAsync(configuration, stream, cancellationToken); - } - - /// - public async Task DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) - => await this.DecodeAsync(configuration, stream, cancellationToken) - .ConfigureAwait(false); - - /// - public IImageInfo Identify(Configuration configuration, Stream stream) - { - Guard.NotNull(stream, nameof(stream)); - - return new BmpDecoderCore(configuration, this).Identify(configuration, stream); - } - - /// - public Task IdentifyAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) - { - Guard.NotNull(stream, nameof(stream)); - - return new BmpDecoderCore(configuration, this).IdentifyAsync(configuration, stream, cancellationToken); + return new BmpDecoderCore(configuration, this).Identify(configuration, stream, cancellationToken); } } } diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index 41adc1cff..26687ff16 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -11,6 +11,7 @@ using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Bmp @@ -116,17 +117,18 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// /// Gets the dimensions of the image. /// - public Size Dimensions => new Size(this.infoHeader.Width, this.infoHeader.Height); + public Size Dimensions => new(this.infoHeader.Width, this.infoHeader.Height); /// public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { + Image image = null; try { int bytesPerColorMapEntry = this.ReadImageHeaders(stream, out bool inverted, out byte[] palette); - var image = new Image(this.Configuration, this.infoHeader.Width, this.infoHeader.Height, this.metadata); + image = new Image(this.Configuration, this.infoHeader.Width, this.infoHeader.Height, this.metadata); Buffer2D pixels = image.GetRootFramePixelBuffer(); @@ -184,7 +186,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp break; default: - BmpThrowHelper.ThrowNotSupportedException("Does not support this kind of bitmap files."); + BmpThrowHelper.ThrowNotSupportedException("ImageSharp does not support this kind of bitmap files."); break; } @@ -193,8 +195,14 @@ namespace SixLabors.ImageSharp.Formats.Bmp } catch (IndexOutOfRangeException e) { + image?.Dispose(); throw new ImageFormatException("Bitmap does not have a valid format.", e); } + catch + { + image?.Dispose(); + throw; + } } /// @@ -323,12 +331,12 @@ namespace SixLabors.ImageSharp.Formats.Bmp color.FromBgr24(Unsafe.As(ref colors[colorIdx * 4])); break; case RleSkippedPixelHandling.Transparent: - color.FromVector4(Vector4.Zero); + color.FromScaledVector4(Vector4.Zero); break; // Default handling for skipped pixels is black (which is what System.Drawing is also doing). default: - color.FromVector4(new Vector4(0.0f, 0.0f, 0.0f, 1.0f)); + color.FromScaledVector4(new Vector4(0.0f, 0.0f, 0.0f, 1.0f)); break; } } @@ -382,7 +390,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp if (rowHasUndefinedPixels) { // Slow path with undefined pixels. - var yMulWidth = y * width; + int yMulWidth = y * width; int rowStartIdx = yMulWidth * 3; for (int x = 0; x < width; x++) { @@ -395,12 +403,12 @@ namespace SixLabors.ImageSharp.Formats.Bmp color.FromBgr24(Unsafe.As(ref bufferSpan[idx])); break; case RleSkippedPixelHandling.Transparent: - color.FromVector4(Vector4.Zero); + color.FromScaledVector4(Vector4.Zero); break; // Default handling for skipped pixels is black (which is what System.Drawing is also doing). default: - color.FromVector4(new Vector4(0.0f, 0.0f, 0.0f, 1.0f)); + color.FromScaledVector4(new Vector4(0.0f, 0.0f, 0.0f, 1.0f)); break; } } @@ -1127,7 +1135,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp g * invMaxValueGreen, b * invMaxValueBlue, alpha); - color.FromVector4(vector4); + color.FromScaledVector4(vector4); } else { @@ -1192,6 +1200,13 @@ namespace SixLabors.ImageSharp.Formats.Bmp private void ReadInfoHeader() { Span buffer = stackalloc byte[BmpInfoHeader.MaxHeaderSize]; + long infoHeaderStart = this.stream.Position; + + // Resolution is stored in PPM. + this.metadata = new ImageMetadata + { + ResolutionUnits = PixelResolutionUnit.PixelsPerMeter + }; // Read the header size. this.stream.Read(buffer, 0, BmpInfoHeader.HeaderSizeSize); @@ -1264,36 +1279,45 @@ namespace SixLabors.ImageSharp.Formats.Bmp infoHeaderType = BmpInfoHeaderType.Os2Version2; this.infoHeader = BmpInfoHeader.ParseOs2Version2(buffer); } - else if (headerSize >= BmpInfoHeader.SizeV4) + else if (headerSize == BmpInfoHeader.SizeV4) { - // >= 108 bytes - infoHeaderType = headerSize == BmpInfoHeader.SizeV4 ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion5; + // == 108 bytes + infoHeaderType = BmpInfoHeaderType.WinVersion4; this.infoHeader = BmpInfoHeader.ParseV4(buffer); } + else if (headerSize > BmpInfoHeader.SizeV4) + { + // > 108 bytes + infoHeaderType = BmpInfoHeaderType.WinVersion5; + this.infoHeader = BmpInfoHeader.ParseV5(buffer); + if (this.infoHeader.ProfileData != 0 && this.infoHeader.ProfileSize != 0) + { + // Read color profile. + long streamPosition = this.stream.Position; + byte[] iccProfileData = new byte[this.infoHeader.ProfileSize]; + this.stream.Position = infoHeaderStart + this.infoHeader.ProfileData; + this.stream.Read(iccProfileData); + this.metadata.IccProfile = new IccProfile(iccProfileData); + this.stream.Position = streamPosition; + } + } else { BmpThrowHelper.ThrowNotSupportedException($"ImageSharp does not support this BMP file. HeaderSize '{headerSize}'."); } - // Resolution is stored in PPM. - var meta = new ImageMetadata - { - ResolutionUnits = PixelResolutionUnit.PixelsPerMeter - }; if (this.infoHeader.XPelsPerMeter > 0 && this.infoHeader.YPelsPerMeter > 0) { - meta.HorizontalResolution = this.infoHeader.XPelsPerMeter; - meta.VerticalResolution = this.infoHeader.YPelsPerMeter; + this.metadata.HorizontalResolution = this.infoHeader.XPelsPerMeter; + this.metadata.VerticalResolution = this.infoHeader.YPelsPerMeter; } else { // Convert default metadata values to PPM. - meta.HorizontalResolution = Math.Round(UnitConverter.InchToMeter(ImageMetadata.DefaultHorizontalResolution)); - meta.VerticalResolution = Math.Round(UnitConverter.InchToMeter(ImageMetadata.DefaultVerticalResolution)); + this.metadata.HorizontalResolution = Math.Round(UnitConverter.InchToMeter(ImageMetadata.DefaultHorizontalResolution)); + this.metadata.VerticalResolution = Math.Round(UnitConverter.InchToMeter(ImageMetadata.DefaultVerticalResolution)); } - this.metadata = meta; - short bitsPerPixel = this.infoHeader.BitsPerPixel; this.bmpMetadata = this.metadata.GetBmpMetadata(); this.bmpMetadata.InfoHeaderType = infoHeaderType; @@ -1363,9 +1387,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp int colorMapSizeBytes = -1; if (this.infoHeader.ClrUsed == 0) { - if (this.infoHeader.BitsPerPixel == 1 - || this.infoHeader.BitsPerPixel == 4 - || this.infoHeader.BitsPerPixel == 8) + if (this.infoHeader.BitsPerPixel is 1 or 4 or 8) { switch (this.fileMarkerType) { @@ -1417,7 +1439,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp int skipAmount = this.fileHeader.Offset - (int)this.stream.Position; if ((skipAmount + (int)this.stream.Position) > this.stream.Length) { - BmpThrowHelper.ThrowInvalidImageContentException("Invalid fileheader offset found. Offset is greater than the stream length."); + BmpThrowHelper.ThrowInvalidImageContentException("Invalid file header offset found. Offset is greater than the stream length."); } if (skipAmount > 0) diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index 6384074df..247ed7811 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -3,6 +3,7 @@ using System; using System.Buffers; +using System.Buffers.Binary; using System.IO; using System.Runtime.InteropServices; using System.Threading; @@ -79,9 +80,10 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// /// A bitmap v4 header will only be written, if the user explicitly wants support for transparency. /// In this case the compression type BITFIELDS will be used. + /// If the image contains a color profile, a bitmap v5 header is written, which is needed to write this info. /// Otherwise a bitmap v3 header will be written, which is supported by almost all decoders. /// - private readonly bool writeV4Header; + private BmpInfoHeaderType infoHeaderType; /// /// The quantizer for reducing the color count for 8-Bit, 4-Bit and 1-Bit images. @@ -97,8 +99,8 @@ namespace SixLabors.ImageSharp.Formats.Bmp { this.memoryAllocator = memoryAllocator; this.bitsPerPixel = options.BitsPerPixel; - this.writeV4Header = options.SupportTransparency; this.quantizer = options.Quantizer ?? KnownQuantizers.Octree; + this.infoHeaderType = options.SupportTransparency ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion3; } /// @@ -123,7 +125,62 @@ namespace SixLabors.ImageSharp.Formats.Bmp int bytesPerLine = 4 * (((image.Width * bpp) + 31) / 32); this.padding = bytesPerLine - (int)(image.Width * (bpp / 8F)); - // Set Resolution. + int colorPaletteSize = 0; + if (this.bitsPerPixel == BmpBitsPerPixel.Pixel8) + { + colorPaletteSize = ColorPaletteSize8Bit; + } + else if (this.bitsPerPixel == BmpBitsPerPixel.Pixel4) + { + colorPaletteSize = ColorPaletteSize4Bit; + } + else if (this.bitsPerPixel == BmpBitsPerPixel.Pixel1) + { + colorPaletteSize = ColorPaletteSize1Bit; + } + + byte[] iccProfileData = null; + int iccProfileSize = 0; + if (metadata.IccProfile != null) + { + this.infoHeaderType = BmpInfoHeaderType.WinVersion5; + iccProfileData = metadata.IccProfile.ToByteArray(); + iccProfileSize = iccProfileData.Length; + } + + int infoHeaderSize = this.infoHeaderType switch + { + BmpInfoHeaderType.WinVersion3 => BmpInfoHeader.SizeV3, + BmpInfoHeaderType.WinVersion4 => BmpInfoHeader.SizeV4, + BmpInfoHeaderType.WinVersion5 => BmpInfoHeader.SizeV5, + _ => BmpInfoHeader.SizeV3 + }; + + BmpInfoHeader infoHeader = this.CreateBmpInfoHeader(image.Width, image.Height, infoHeaderSize, bpp, bytesPerLine, metadata, iccProfileData); + + Span buffer = stackalloc byte[infoHeaderSize]; + + this.WriteBitmapFileHeader(stream, infoHeaderSize, colorPaletteSize, iccProfileSize, infoHeader, buffer); + this.WriteBitmapInfoHeader(stream, infoHeader, buffer, infoHeaderSize); + this.WriteImage(stream, image.Frames.RootFrame); + this.WriteColorProfile(stream, iccProfileData, buffer); + + stream.Flush(); + } + + /// + /// Creates the bitmap information header. + /// + /// The width of the image. + /// The height of the image. + /// Size of the information header. + /// The bits per pixel. + /// The bytes per line. + /// The metadata. + /// The icc profile data. + /// The bitmap information header. + private BmpInfoHeader CreateBmpInfoHeader(int width, int height, int infoHeaderSize, short bpp, int bytesPerLine, ImageMetadata metadata, byte[] iccProfileData) + { int hResolution = 0; int vResolution = 0; @@ -154,20 +211,19 @@ namespace SixLabors.ImageSharp.Formats.Bmp } } - int infoHeaderSize = this.writeV4Header ? BmpInfoHeader.SizeV4 : BmpInfoHeader.SizeV3; var infoHeader = new BmpInfoHeader( headerSize: infoHeaderSize, - height: image.Height, - width: image.Width, + height: height, + width: width, bitsPerPixel: bpp, planes: 1, - imageSize: image.Height * bytesPerLine, + imageSize: height * bytesPerLine, clrUsed: 0, clrImportant: 0, xPelsPerMeter: hResolution, yPelsPerMeter: vResolution); - if (this.writeV4Header && this.bitsPerPixel == BmpBitsPerPixel.Pixel32) + if ((this.infoHeaderType is BmpInfoHeaderType.WinVersion4 or BmpInfoHeaderType.WinVersion5) && this.bitsPerPixel == BmpBitsPerPixel.Pixel32) { infoHeader.AlphaMask = Rgba32AlphaMask; infoHeader.RedMask = Rgba32RedMask; @@ -176,45 +232,79 @@ namespace SixLabors.ImageSharp.Formats.Bmp infoHeader.Compression = BmpCompression.BitFields; } - int colorPaletteSize = 0; - if (this.bitsPerPixel == BmpBitsPerPixel.Pixel8) + if (this.infoHeaderType is BmpInfoHeaderType.WinVersion5 && metadata.IccProfile != null) { - colorPaletteSize = ColorPaletteSize8Bit; + infoHeader.ProfileSize = iccProfileData.Length; + infoHeader.CsType = BmpColorSpace.PROFILE_EMBEDDED; + infoHeader.Intent = BmpRenderingIntent.LCS_GM_IMAGES; } - else if (this.bitsPerPixel == BmpBitsPerPixel.Pixel4) - { - colorPaletteSize = ColorPaletteSize4Bit; - } - else if (this.bitsPerPixel == BmpBitsPerPixel.Pixel1) + + return infoHeader; + } + + /// + /// Writes the color profile to the stream. + /// + /// The stream to write to. + /// The color profile data. + /// The buffer. + private void WriteColorProfile(Stream stream, byte[] iccProfileData, Span buffer) + { + if (iccProfileData != null) { - colorPaletteSize = ColorPaletteSize1Bit; + // The offset, in bytes, from the beginning of the BITMAPV5HEADER structure to the start of the profile data. + int streamPositionAfterImageData = (int)stream.Position - BmpFileHeader.Size; + stream.Write(iccProfileData); + BinaryPrimitives.WriteInt32LittleEndian(buffer, streamPositionAfterImageData); + stream.Position = BmpFileHeader.Size + 112; + stream.Write(buffer.Slice(0, 4)); } + } + /// + /// Writes the bitmap file header. + /// + /// The stream to write the header to. + /// Size of the bitmap information header. + /// Size of the color palette. + /// The size in bytes of the color profile. + /// The information header to write. + /// The buffer to write to. + private void WriteBitmapFileHeader(Stream stream, int infoHeaderSize, int colorPaletteSize, int iccProfileSize, BmpInfoHeader infoHeader, Span buffer) + { var fileHeader = new BmpFileHeader( type: BmpConstants.TypeMarkers.Bitmap, - fileSize: BmpFileHeader.Size + infoHeaderSize + colorPaletteSize + infoHeader.ImageSize, + fileSize: BmpFileHeader.Size + infoHeaderSize + colorPaletteSize + iccProfileSize + infoHeader.ImageSize, reserved: 0, offset: BmpFileHeader.Size + infoHeaderSize + colorPaletteSize); - Span buffer = stackalloc byte[infoHeaderSize]; fileHeader.WriteTo(buffer); - stream.Write(buffer, 0, BmpFileHeader.Size); + } - if (this.writeV4Header) - { - infoHeader.WriteV4Header(buffer); - } - else + /// + /// Writes the bitmap information header. + /// + /// The stream to write info header into. + /// The information header. + /// The buffer. + /// Size of the information header. + private void WriteBitmapInfoHeader(Stream stream, BmpInfoHeader infoHeader, Span buffer, int infoHeaderSize) + { + switch (this.infoHeaderType) { - infoHeader.WriteV3Header(buffer); + case BmpInfoHeaderType.WinVersion3: + infoHeader.WriteV3Header(buffer); + break; + case BmpInfoHeaderType.WinVersion4: + infoHeader.WriteV4Header(buffer); + break; + case BmpInfoHeaderType.WinVersion5: + infoHeader.WriteV5Header(buffer); + break; } stream.Write(buffer, 0, infoHeaderSize); - - this.WriteImage(stream, image.Frames.RootFrame); - - stream.Flush(); } /// diff --git a/src/ImageSharp/Formats/Bmp/BmpFileHeader.cs b/src/ImageSharp/Formats/Bmp/BmpFileHeader.cs index acbcdaef3..ab56bd246 100644 --- a/src/ImageSharp/Formats/Bmp/BmpFileHeader.cs +++ b/src/ImageSharp/Formats/Bmp/BmpFileHeader.cs @@ -57,10 +57,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// public int Offset { get; } - public static BmpFileHeader Parse(Span data) - { - return MemoryMarshal.Cast(data)[0]; - } + public static BmpFileHeader Parse(Span data) => MemoryMarshal.Cast(data)[0]; public void WriteTo(Span buffer) { diff --git a/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs b/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs index 0d0c05c9f..31394821f 100644 --- a/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs +++ b/src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs @@ -82,7 +82,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp int greenMask = 0, int blueMask = 0, int alphaMask = 0, - int csType = 0, + BmpColorSpace csType = 0, int redX = 0, int redY = 0, int redZ = 0, @@ -94,7 +94,11 @@ namespace SixLabors.ImageSharp.Formats.Bmp int blueZ = 0, int gammeRed = 0, int gammeGreen = 0, - int gammeBlue = 0) + int gammeBlue = 0, + BmpRenderingIntent intent = BmpRenderingIntent.Invalid, + int profileData = 0, + int profileSize = 0, + int reserved = 0) { this.HeaderSize = headerSize; this.Width = width; @@ -124,6 +128,10 @@ namespace SixLabors.ImageSharp.Formats.Bmp this.GammaRed = gammeRed; this.GammaGreen = gammeGreen; this.GammaBlue = gammeBlue; + this.Intent = intent; + this.ProfileData = profileData; + this.ProfileSize = profileSize; + this.Reserved = reserved; } /// @@ -211,7 +219,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// /// Gets or sets the Color space type. Not used yet. /// - public int CsType { get; set; } + public BmpColorSpace CsType { get; set; } /// /// Gets or sets the X coordinate of red endpoint. Not used yet. @@ -273,21 +281,38 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// public int GammaBlue { get; set; } + /// + /// Gets or sets the rendering intent for bitmap. + /// + public BmpRenderingIntent Intent { get; set; } + + /// + /// Gets or sets the offset, in bytes, from the beginning of the BITMAPV5HEADER structure to the start of the profile data. + /// + public int ProfileData { get; set; } + + /// + /// Gets or sets the size, in bytes, of embedded profile data. + /// + public int ProfileSize { get; set; } + + /// + /// Gets or sets the reserved value. + /// + public int Reserved { get; set; } + /// /// Parses the BITMAPCOREHEADER (BMP Version 2) consisting of the headerSize, width, height, planes, and bitsPerPixel fields (12 bytes). /// /// The data to parse. /// The parsed header. /// - public static BmpInfoHeader ParseCore(ReadOnlySpan data) - { - return new BmpInfoHeader( + public static BmpInfoHeader ParseCore(ReadOnlySpan data) => new( headerSize: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(0, 4)), width: BinaryPrimitives.ReadUInt16LittleEndian(data.Slice(4, 2)), height: BinaryPrimitives.ReadUInt16LittleEndian(data.Slice(6, 2)), planes: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(8, 2)), bitsPerPixel: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(10, 2))); - } /// /// Parses a short variant of the OS22XBITMAPHEADER. It is identical to the BITMAPCOREHEADER, except that the width and height @@ -296,15 +321,12 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// The data to parse. /// The parsed header. /// - public static BmpInfoHeader ParseOs22Short(ReadOnlySpan data) - { - return new BmpInfoHeader( + public static BmpInfoHeader ParseOs22Short(ReadOnlySpan data) => new( headerSize: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(0, 4)), width: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4)), height: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4)), planes: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(12, 2)), bitsPerPixel: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(14, 2))); - } /// /// Parses the full BMP Version 3 BITMAPINFOHEADER header (40 bytes). @@ -312,9 +334,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// The data to parse. /// The parsed header. /// - public static BmpInfoHeader ParseV3(ReadOnlySpan data) - { - return new BmpInfoHeader( + public static BmpInfoHeader ParseV3(ReadOnlySpan data) => new( headerSize: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(0, 4)), width: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4)), height: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4)), @@ -326,7 +346,6 @@ namespace SixLabors.ImageSharp.Formats.Bmp yPelsPerMeter: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(28, 4)), clrUsed: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(32, 4)), clrImportant: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(36, 4))); - } /// /// Special case of the BITMAPINFOHEADER V3 used by adobe where the color bitmasks are part of the info header instead of following it. @@ -336,9 +355,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// Indicates, if the alpha bitmask is present. /// The parsed header. /// - public static BmpInfoHeader ParseAdobeV3(ReadOnlySpan data, bool withAlpha = true) - { - return new BmpInfoHeader( + public static BmpInfoHeader ParseAdobeV3(ReadOnlySpan data, bool withAlpha = true) => new( headerSize: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(0, 4)), width: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4)), height: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4)), @@ -354,7 +371,6 @@ namespace SixLabors.ImageSharp.Formats.Bmp greenMask: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(44, 4)), blueMask: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(48, 4)), alphaMask: withAlpha ? BinaryPrimitives.ReadInt32LittleEndian(data.Slice(52, 4)) : 0); - } /// /// Parses a OS/2 version 2 bitmap header (64 bytes). Only the first 40 bytes are parsed which are @@ -413,11 +429,47 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// The data to parse. /// The parsed header. /// - public static BmpInfoHeader ParseV4(ReadOnlySpan data) + public static BmpInfoHeader ParseV4(ReadOnlySpan data) => new( + headerSize: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(0, 4)), + width: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4)), + height: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(8, 4)), + planes: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(12, 2)), + bitsPerPixel: BinaryPrimitives.ReadInt16LittleEndian(data.Slice(14, 2)), + compression: (BmpCompression)BinaryPrimitives.ReadInt32LittleEndian(data.Slice(16, 4)), + imageSize: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(20, 4)), + xPelsPerMeter: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(24, 4)), + yPelsPerMeter: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(28, 4)), + clrUsed: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(32, 4)), + clrImportant: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(36, 4)), + redMask: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(40, 4)), + greenMask: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(44, 4)), + blueMask: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(48, 4)), + alphaMask: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(52, 4)), + csType: (BmpColorSpace)BinaryPrimitives.ReadInt32LittleEndian(data.Slice(56, 4)), + redX: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(60, 4)), + redY: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(64, 4)), + redZ: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(68, 4)), + greenX: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(72, 4)), + greenY: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(76, 4)), + greenZ: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(80, 4)), + blueX: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(84, 4)), + blueY: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(88, 4)), + blueZ: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(92, 4)), + gammeRed: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(96, 4)), + gammeGreen: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(100, 4)), + gammeBlue: BinaryPrimitives.ReadInt32LittleEndian(data.Slice(104, 4))); + + /// + /// Parses the full BMP Version 5 BITMAPINFOHEADER header (124 bytes). + /// + /// The data to parse. + /// The parsed header. + /// + public static BmpInfoHeader ParseV5(ReadOnlySpan data) { - if (data.Length < SizeV4) + if (data.Length < SizeV5) { - throw new ArgumentException(nameof(data), $"Must be {SizeV4} bytes. Was {data.Length} bytes."); + throw new ArgumentException(nameof(data), $"Must be {SizeV5} bytes. Was {data.Length} bytes."); } return MemoryMarshal.Cast(data)[0]; @@ -448,6 +500,43 @@ namespace SixLabors.ImageSharp.Formats.Bmp /// /// The buffer to write to. public void WriteV4Header(Span buffer) + { + buffer.Clear(); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(0, 4), SizeV4); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(4, 4), this.Width); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(8, 4), this.Height); + BinaryPrimitives.WriteInt16LittleEndian(buffer.Slice(12, 2), this.Planes); + BinaryPrimitives.WriteInt16LittleEndian(buffer.Slice(14, 2), this.BitsPerPixel); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(16, 4), (int)this.Compression); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(20, 4), this.ImageSize); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(24, 4), this.XPelsPerMeter); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(28, 4), this.YPelsPerMeter); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(32, 4), this.ClrUsed); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(36, 4), this.ClrImportant); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(40, 4), this.RedMask); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(44, 4), this.GreenMask); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(48, 4), this.BlueMask); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(52, 4), this.AlphaMask); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(56, 4), (int)this.CsType); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(60, 4), this.RedX); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(64, 4), this.RedY); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(68, 4), this.RedZ); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(72, 4), this.GreenX); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(76, 4), this.GreenY); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(80, 4), this.GreenZ); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(84, 4), this.BlueX); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(88, 4), this.BlueY); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(92, 4), this.BlueZ); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(96, 4), this.GammaRed); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(100, 4), this.GammaGreen); + BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(104, 4), this.GammaBlue); + } + + /// + /// Writes a complete Bitmap V5 header to a buffer. + /// + /// The buffer to write to. + public void WriteV5Header(Span buffer) { ref BmpInfoHeader dest = ref Unsafe.As(ref MemoryMarshal.GetReference(buffer)); diff --git a/src/ImageSharp/Formats/Bmp/BmpRenderingIntent.cs b/src/ImageSharp/Formats/Bmp/BmpRenderingIntent.cs new file mode 100644 index 000000000..e437a0cbf --- /dev/null +++ b/src/ImageSharp/Formats/Bmp/BmpRenderingIntent.cs @@ -0,0 +1,37 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +// ReSharper disable InconsistentNaming +namespace SixLabors.ImageSharp.Formats.Bmp +{ + /// + /// Enum for the different rendering intent's. + /// + internal enum BmpRenderingIntent + { + /// + /// Invalid default value. + /// + Invalid = 0, + + /// + /// Maintains saturation. Used for business charts and other situations in which undithered colors are required. + /// + LCS_GM_BUSINESS = 1, + + /// + /// Maintains colorimetric match. Used for graphic designs and named colors. + /// + LCS_GM_GRAPHICS = 2, + + /// + /// Maintains contrast. Used for photographs and natural images. + /// + LCS_GM_IMAGES = 4, + + /// + /// Maintains the white point. Matches the colors to their nearest color in the destination gamut. + /// + LCS_GM_ABS_COLORIMETRIC = 8, + } +} diff --git a/src/ImageSharp/Formats/Gif/GifDecoder.cs b/src/ImageSharp/Formats/Gif/GifDecoder.cs index 196d77ad7..6d6cfc079 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoder.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoder.cs @@ -3,9 +3,6 @@ using System.IO; using System.Threading; -using System.Threading.Tasks; -using SixLabors.ImageSharp.IO; -using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; @@ -27,48 +24,24 @@ namespace SixLabors.ImageSharp.Formats.Gif public FrameDecodingMode DecodingMode { get; set; } = FrameDecodingMode.All; /// - public Image Decode(Configuration configuration, Stream stream) + public Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { var decoder = new GifDecoderCore(configuration, this); - return decoder.Decode(configuration, stream); + return decoder.Decode(configuration, stream, cancellationToken); } /// - public Image Decode(Configuration configuration, Stream stream) - => this.Decode(configuration, stream); + public Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken) + => this.Decode(configuration, stream, cancellationToken); /// - public Task> DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel - { - var decoder = new GifDecoderCore(configuration, this); - return decoder.DecodeAsync(configuration, stream, cancellationToken); - } - - /// - public async Task DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) - => await this.DecodeAsync(configuration, stream, cancellationToken) - .ConfigureAwait(false); - - /// - public IImageInfo Identify(Configuration configuration, Stream stream) - { - Guard.NotNull(stream, nameof(stream)); - - var decoder = new GifDecoderCore(configuration, this); - - using var bufferedStream = new BufferedReadStream(configuration, stream); - return decoder.Identify(bufferedStream, default); - } - - /// - public Task IdentifyAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) + public IImageInfo Identify(Configuration configuration, Stream stream, CancellationToken cancellationToken) { Guard.NotNull(stream, nameof(stream)); var decoder = new GifDecoderCore(configuration, this); - return decoder.IdentifyAsync(configuration, stream, cancellationToken); + return decoder.Identify(configuration, stream, cancellationToken); } } } diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index b6348803a..d17e89cd4 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -221,7 +221,11 @@ namespace SixLabors.ImageSharp.Formats.Gif /// private void ReadGraphicalControlExtension() { - this.stream.Read(this.buffer, 0, 6); + int bytesRead = this.stream.Read(this.buffer, 0, 6); + if (bytesRead != 6) + { + GifThrowHelper.ThrowInvalidImageContentException("Not enough data to read the graphic control extension"); + } this.graphicsControlExtension = GifGraphicControlExtension.Parse(this.buffer); } @@ -231,7 +235,11 @@ namespace SixLabors.ImageSharp.Formats.Gif /// private void ReadImageDescriptor() { - this.stream.Read(this.buffer, 0, 9); + int bytesRead = this.stream.Read(this.buffer, 0, 9); + if (bytesRead != 9) + { + GifThrowHelper.ThrowInvalidImageContentException("Not enough data to read the image descriptor"); + } this.imageDescriptor = GifImageDescriptor.Parse(this.buffer); if (this.imageDescriptor.Height == 0 || this.imageDescriptor.Width == 0) @@ -245,7 +253,11 @@ namespace SixLabors.ImageSharp.Formats.Gif /// private void ReadLogicalScreenDescriptor() { - this.stream.Read(this.buffer, 0, 7); + int bytesRead = this.stream.Read(this.buffer, 0, 7); + if (bytesRead != 7) + { + GifThrowHelper.ThrowInvalidImageContentException("Not enough data to read the logical screen descriptor"); + } this.logicalScreenDescriptor = GifLogicalScreenDescriptor.Parse(this.buffer); } @@ -265,10 +277,14 @@ namespace SixLabors.ImageSharp.Formats.Gif this.stream.Read(this.buffer, 0, GifConstants.ApplicationBlockSize); bool isXmp = this.buffer.AsSpan().StartsWith(GifConstants.XmpApplicationIdentificationBytes); - if (isXmp) + if (isXmp && !this.IgnoreMetadata) { - var extension = GifXmpApplicationExtension.Read(this.stream); - this.metadata.XmpProfile = new XmpProfile(extension.Data); + var extension = GifXmpApplicationExtension.Read(this.stream, this.MemoryAllocator); + if (extension.Data.Length > 0) + { + this.metadata.XmpProfile = new XmpProfile(extension.Data); + } + return; } else @@ -374,9 +390,19 @@ namespace SixLabors.ImageSharp.Formats.Gif } indices = this.Configuration.MemoryAllocator.Allocate2D(this.imageDescriptor.Width, this.imageDescriptor.Height, AllocationOptions.Clean); - this.ReadFrameIndices(indices); - ReadOnlySpan colorTable = MemoryMarshal.Cast((localColorTable ?? this.globalColorTable).GetSpan()); + + Span rawColorTable = default; + if (localColorTable != null) + { + rawColorTable = localColorTable.GetSpan(); + } + else if (this.globalColorTable != null) + { + rawColorTable = this.globalColorTable.GetSpan(); + } + + ReadOnlySpan colorTable = MemoryMarshal.Cast(rawColorTable); this.ReadFrameColors(ref image, ref previousFrame, indices, colorTable, this.imageDescriptor); // Skip any remaining blocks @@ -396,9 +422,9 @@ namespace SixLabors.ImageSharp.Formats.Gif [MethodImpl(MethodImplOptions.AggressiveInlining)] private void ReadFrameIndices(Buffer2D indices) { - int dataSize = this.stream.ReadByte(); + int minCodeSize = this.stream.ReadByte(); using var lzwDecoder = new LzwDecoder(this.Configuration.MemoryAllocator, this.stream); - lzwDecoder.DecodePixels(dataSize, indices); + lzwDecoder.DecodePixels(minCodeSize, indices); } /// @@ -415,6 +441,7 @@ namespace SixLabors.ImageSharp.Formats.Gif { int imageWidth = this.logicalScreenDescriptor.Width; int imageHeight = this.logicalScreenDescriptor.Height; + bool transFlag = this.graphicsControlExtension.TransparencyFlag; ImageFrame prevFrame = null; ImageFrame currentFrame = null; @@ -422,8 +449,15 @@ namespace SixLabors.ImageSharp.Formats.Gif if (previousFrame is null) { - // This initializes the image to become fully transparent because the alpha channel is zero. - image = new Image(this.Configuration, imageWidth, imageHeight, this.metadata); + if (!transFlag) + { + image = new Image(this.Configuration, imageWidth, imageHeight, Color.Black.ToPixel(), this.metadata); + } + else + { + // This initializes the image to become fully transparent because the alpha channel is zero. + image = new Image(this.Configuration, imageWidth, imageHeight, this.metadata); + } this.SetFrameMetadata(image.Frames.RootFrame.Metadata); @@ -445,6 +479,11 @@ namespace SixLabors.ImageSharp.Formats.Gif this.RestoreToBackground(imageFrame); } + if (colorTable.Length == 0) + { + return; + } + int interlacePass = 0; // The interlace pass int interlaceIncrement = 8; // The interlacing line increment int interlaceY = 0; // The current interlaced line @@ -452,7 +491,6 @@ namespace SixLabors.ImageSharp.Formats.Gif int descriptorBottom = descriptorTop + descriptor.Height; int descriptorLeft = descriptor.Left; int descriptorRight = descriptorLeft + descriptor.Width; - bool transFlag = this.graphicsControlExtension.TransparencyFlag; byte transIndex = this.graphicsControlExtension.TransparencyIndex; int colorTableMaxIdx = colorTable.Length - 1; @@ -635,10 +673,13 @@ namespace SixLabors.ImageSharp.Formats.Gif int globalColorTableLength = this.logicalScreenDescriptor.GlobalColorTableSize * 3; this.gifMetadata.GlobalColorTableLength = globalColorTableLength; - this.globalColorTable = this.MemoryAllocator.Allocate(globalColorTableLength, AllocationOptions.Clean); + if (globalColorTableLength > 0) + { + this.globalColorTable = this.MemoryAllocator.Allocate(globalColorTableLength, AllocationOptions.Clean); - // Read the global color table data from the stream - stream.Read(this.globalColorTable.GetSpan()); + // Read the global color table data from the stream + stream.Read(this.globalColorTable.GetSpan()); + } } } } diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index a21b050a8..da5b1cb23 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -123,7 +123,8 @@ namespace SixLabors.ImageSharp.Formats.Gif this.WriteComments(gifMetadata, stream); // Write application extensions. - this.WriteApplicationExtensions(stream, image.Frames.Count, gifMetadata.RepeatCount, metadata.XmpProfile); + XmpProfile xmpProfile = image.Metadata.XmpProfile ?? image.Frames.RootFrame.Metadata.XmpProfile; + this.WriteApplicationExtensions(stream, image.Frames.Count, gifMetadata.RepeatCount, xmpProfile); if (useGlobalTable) { @@ -137,7 +138,6 @@ namespace SixLabors.ImageSharp.Formats.Gif // Clean up. quantized.Dispose(); - // TODO: Write extension etc stream.WriteByte(GifConstants.EndIntroducer); } @@ -428,26 +428,31 @@ namespace SixLabors.ImageSharp.Formats.Gif where TGifExtension : struct, IGifExtension { IMemoryOwner owner = null; - Span buffer; + Span extensionBuffer; int extensionSize = extension.ContentLength; - if (extensionSize > this.buffer.Length - 3) + + if (extensionSize == 0) + { + return; + } + else if (extensionSize > this.buffer.Length - 3) { owner = this.memoryAllocator.Allocate(extensionSize + 3); - buffer = owner.GetSpan(); + extensionBuffer = owner.GetSpan(); } else { - buffer = this.buffer; + extensionBuffer = this.buffer; } - buffer[0] = GifConstants.ExtensionIntroducer; - buffer[1] = extension.Label; + extensionBuffer[0] = GifConstants.ExtensionIntroducer; + extensionBuffer[1] = extension.Label; - extension.WriteTo(buffer.Slice(2)); + extension.WriteTo(extensionBuffer.Slice(2)); - buffer[extensionSize + 2] = GifConstants.Terminator; + extensionBuffer[extensionSize + 2] = GifConstants.Terminator; - stream.Write(buffer, 0, extensionSize + 3); + stream.Write(extensionBuffer, 0, extensionSize + 3); owner?.Dispose(); } diff --git a/src/ImageSharp/Formats/Gif/LzwDecoder.cs b/src/ImageSharp/Formats/Gif/LzwDecoder.cs index 68227db53..2a0720001 100644 --- a/src/ImageSharp/Formats/Gif/LzwDecoder.cs +++ b/src/ImageSharp/Formats/Gif/LzwDecoder.cs @@ -64,21 +64,30 @@ namespace SixLabors.ImageSharp.Formats.Gif /// /// Decodes and decompresses all pixel indices from the stream. /// - /// Size of the data. + /// Minimum code size of the data. /// The pixel array to decode to. - public void DecodePixels(int dataSize, Buffer2D pixels) + public void DecodePixels(int minCodeSize, Buffer2D pixels) { - Guard.MustBeLessThan(dataSize, int.MaxValue, nameof(dataSize)); + // Calculate the clear code. The value of the clear code is 2 ^ minCodeSize + int clearCode = 1 << minCodeSize; + + // It is possible to specify a larger LZW minimum code size than the palette length in bits + // which may leave a gap in the codes where no colors are assigned. + // http://www.matthewflickinger.com/lab/whatsinagif/lzw_image_data.asp#lzw_compression + if (minCodeSize < 2 || clearCode > MaxStackSize) + { + // Don't attempt to decode the frame indices. + // Theoretically we could determine a min code size from the length of the provided + // color palette but we won't bother since the image is most likely corrupted. + GifThrowHelper.ThrowInvalidImageContentException("Gif Image does not contain a valid LZW minimum code."); + } // The resulting index table length. int width = pixels.Width; int height = pixels.Height; int length = width * height; - // Calculate the clear code. The value of the clear code is 2 ^ dataSize - int clearCode = 1 << dataSize; - - int codeSize = dataSize + 1; + int codeSize = minCodeSize + 1; // Calculate the end code int endCode = clearCode + 1; @@ -165,7 +174,7 @@ namespace SixLabors.ImageSharp.Formats.Gif if (code == clearCode) { // Reset the decoder - codeSize = dataSize + 1; + codeSize = minCodeSize + 1; codeMask = (1 << codeSize) - 1; availableCode = clearCode + 2; oldCode = NullCode; diff --git a/src/ImageSharp/Formats/Gif/Sections/GifGraphicControlExtension.cs b/src/ImageSharp/Formats/Gif/Sections/GifGraphicControlExtension.cs index 801849c9b..847633694 100644 --- a/src/ImageSharp/Formats/Gif/Sections/GifGraphicControlExtension.cs +++ b/src/ImageSharp/Formats/Gif/Sections/GifGraphicControlExtension.cs @@ -71,13 +71,11 @@ namespace SixLabors.ImageSharp.Formats.Gif dest = this; - return 5; + return ((IGifExtension)this).ContentLength; } public static GifGraphicControlExtension Parse(ReadOnlySpan buffer) - { - return MemoryMarshal.Cast(buffer)[0]; - } + => MemoryMarshal.Cast(buffer)[0]; public static byte GetPackedValue(GifDisposalMethod disposalMethod, bool userInputFlag = false, bool transparencyFlag = false) { diff --git a/src/ImageSharp/Formats/Gif/Sections/GifNetscapeLoopingApplicationExtension.cs b/src/ImageSharp/Formats/Gif/Sections/GifNetscapeLoopingApplicationExtension.cs index 2c7bed611..c9e8033db 100644 --- a/src/ImageSharp/Formats/Gif/Sections/GifNetscapeLoopingApplicationExtension.cs +++ b/src/ImageSharp/Formats/Gif/Sections/GifNetscapeLoopingApplicationExtension.cs @@ -40,7 +40,7 @@ namespace SixLabors.ImageSharp.Formats.Gif // 0 means loop indefinitely. Count is set as play n + 1 times. BinaryPrimitives.WriteUInt16LittleEndian(buffer.Slice(14, 2), this.RepeatCount); - return 16; // Length - Introducer + Label + Terminator. + return this.ContentLength; // Length - Introducer + Label + Terminator. } } } diff --git a/src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs b/src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs index 236508fe9..8c396e7fb 100644 --- a/src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs +++ b/src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs @@ -2,9 +2,9 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Collections.Generic; using System.IO; -using SixLabors.ImageSharp.Metadata.Profiles.Xmp; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Gif { @@ -14,7 +14,10 @@ namespace SixLabors.ImageSharp.Formats.Gif public byte Label => GifConstants.ApplicationExtensionLabel; - public int ContentLength => this.Data.Length + 269; // 12 + Data Length + 1 + 256 + // size : 1 + // identifier : 11 + // magic trailer : 257 + public int ContentLength => (this.Data.Length > 0) ? this.Data.Length + 269 : 0; /// /// Gets the raw Data. @@ -25,51 +28,28 @@ namespace SixLabors.ImageSharp.Formats.Gif /// Reads the XMP metadata from the specified stream. /// /// The stream to read from. + /// The memory allocator. /// The XMP metadata /// Thrown if the XMP block is not properly terminated. - public static GifXmpApplicationExtension Read(Stream stream) + public static GifXmpApplicationExtension Read(Stream stream, MemoryAllocator allocator) { - // Read data in blocks, until an \0 character is encountered. - // We overshoot, indicated by the terminatorIndex variable. - const int bufferSize = 256; - var list = new List(); - int terminationIndex = -1; - while (terminationIndex < 0) - { - byte[] temp = new byte[bufferSize]; - int bytesRead = stream.Read(temp); - list.Add(temp); - terminationIndex = Array.IndexOf(temp, (byte)1); - } + byte[] xmpBytes = ReadXmpData(stream, allocator); - // Pack all the blocks (except magic trailer) into one single array again. - int dataSize = ((list.Count - 1) * bufferSize) + terminationIndex; - byte[] buffer = new byte[dataSize]; - Span bufferSpan = buffer; - int pos = 0; - for (int j = 0; j < list.Count - 1; j++) + // Exclude the "magic trailer", see XMP Specification Part 3, 1.1.2 GIF + int xmpLength = xmpBytes.Length - 256; // 257 - unread 0x0 + byte[] buffer = Array.Empty(); + if (xmpLength > 0) { - list[j].CopyTo(bufferSpan.Slice(pos)); - pos += bufferSize; + buffer = new byte[xmpLength]; + xmpBytes.AsSpan(0, xmpLength).CopyTo(buffer); + stream.Skip(1); // Skip the terminator. } - // Last one only needs the portion until terminationIndex copied over. - Span lastBytes = list[list.Count - 1]; - lastBytes.Slice(0, terminationIndex).CopyTo(bufferSpan.Slice(pos)); - - // Skip the remainder of the magic trailer. - stream.Skip(258 - (bufferSize - terminationIndex)); return new GifXmpApplicationExtension(buffer); } public int WriteTo(Span buffer) { - int totalSize = this.ContentLength; - if (buffer.Length < totalSize) - { - throw new InsufficientMemoryException("Unable to write XMP metadata to GIF image"); - } - int bytesWritten = 0; buffer[bytesWritten++] = GifConstants.ApplicationBlockSize; @@ -91,7 +71,28 @@ namespace SixLabors.ImageSharp.Formats.Gif buffer[bytesWritten++] = 0x00; - return totalSize; + return this.ContentLength; + } + + private static byte[] ReadXmpData(Stream stream, MemoryAllocator allocator) + { + using ChunkedMemoryStream bytes = new(allocator); + + // XMP data doesn't have a fixed length nor is there an indicator of the length. + // So we simply read one byte at a time until we hit the 0x0 value at the end + // of the magic trailer or the end of the stream. + // Using ChunkedMemoryStream reduces the array resize allocation normally associated + // with writing from a non fixed-size buffer. + while (true) + { + int b = stream.ReadByte(); + if (b <= 0) + { + return bytes.ToArray(); + } + + bytes.WriteByte((byte)b); + } } } } diff --git a/src/ImageSharp/Formats/IImageDecoder.cs b/src/ImageSharp/Formats/IImageDecoder.cs index b55f1119b..db7f64ee2 100644 --- a/src/ImageSharp/Formats/IImageDecoder.cs +++ b/src/ImageSharp/Formats/IImageDecoder.cs @@ -3,7 +3,6 @@ using System.IO; using System.Threading; -using System.Threading.Tasks; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats @@ -13,26 +12,6 @@ namespace SixLabors.ImageSharp.Formats /// public interface IImageDecoder { - /// - /// Decodes the image from the specified stream to an of a specific pixel type. - /// - /// The pixel format. - /// The configuration for the image. - /// The containing image data. - /// The . - // TODO: Document ImageFormatExceptions (https://github.com/SixLabors/ImageSharp/issues/1110) - Image Decode(Configuration configuration, Stream stream) - where TPixel : unmanaged, IPixel; - - /// - /// Decodes the image from the specified stream to an . - /// - /// The configuration for the image. - /// The containing image data. - /// The . - // TODO: Document ImageFormatExceptions (https://github.com/SixLabors/ImageSharp/issues/1110) - Image Decode(Configuration configuration, Stream stream); - /// /// Decodes the image from the specified stream to an of a specific pixel type. /// @@ -42,7 +21,7 @@ namespace SixLabors.ImageSharp.Formats /// The token to monitor for cancellation requests. /// The . // TODO: Document ImageFormatExceptions (https://github.com/SixLabors/ImageSharp/issues/1110) - Task> DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) + Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel; /// @@ -53,6 +32,6 @@ namespace SixLabors.ImageSharp.Formats /// The token to monitor for cancellation requests. /// The . // TODO: Document ImageFormatExceptions (https://github.com/SixLabors/ImageSharp/issues/1110) - Task DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken); + Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken); } } diff --git a/src/ImageSharp/Formats/IImageInfoDetector.cs b/src/ImageSharp/Formats/IImageInfoDetector.cs index 6f5fc2333..c6377edd0 100644 --- a/src/ImageSharp/Formats/IImageInfoDetector.cs +++ b/src/ImageSharp/Formats/IImageInfoDetector.cs @@ -3,7 +3,6 @@ using System.IO; using System.Threading; -using System.Threading.Tasks; namespace SixLabors.ImageSharp.Formats { @@ -12,14 +11,6 @@ namespace SixLabors.ImageSharp.Formats /// public interface IImageInfoDetector { - /// - /// Reads the raw image information from the specified stream. - /// - /// The configuration for the image. - /// The containing image data. - /// The object - IImageInfo Identify(Configuration configuration, Stream stream); - /// /// Reads the raw image information from the specified stream. /// @@ -27,6 +18,6 @@ namespace SixLabors.ImageSharp.Formats /// The containing image data. /// The token to monitor for cancellation requests. /// The object - Task IdentifyAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken); + IImageInfo Identify(Configuration configuration, Stream stream, CancellationToken cancellationToken); } } diff --git a/src/ImageSharp/Formats/ImageDecoderUtilities.cs b/src/ImageSharp/Formats/ImageDecoderUtilities.cs index 5d77fb0c8..71ecda893 100644 --- a/src/ImageSharp/Formats/ImageDecoderUtilities.cs +++ b/src/ImageSharp/Formats/ImageDecoderUtilities.cs @@ -4,7 +4,6 @@ using System; using System.IO; using System.Threading; -using System.Threading.Tasks; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -13,153 +12,45 @@ namespace SixLabors.ImageSharp.Formats { internal static class ImageDecoderUtilities { - /// - /// Reads the raw image information from the specified stream. - /// - /// The decoder. - /// /// The configuration for the image. - /// The containing image data. - /// The token to monitor for cancellation requests. - /// is null. - /// A representing the asynchronous operation. - public static Task IdentifyAsync( - this IImageDecoderInternals decoder, - Configuration configuration, - Stream stream, - CancellationToken cancellationToken) - => decoder.IdentifyAsync(configuration, stream, DefaultLargeImageExceptionFactory, cancellationToken); - - /// - /// Reads the raw image information from the specified stream. - /// - /// The decoder. - /// The configuration for the image. - /// The containing image data. - /// Factory method to handle as . - /// The token to monitor for cancellation requests. - /// is null. - /// A representing the asynchronous operation. - public static Task IdentifyAsync( + public static IImageInfo Identify( this IImageDecoderInternals decoder, Configuration configuration, Stream stream, - Func tooLargeImageExceptionFactory, CancellationToken cancellationToken) { + using var bufferedReadStream = new BufferedReadStream(configuration, stream); + try { - using var bufferedReadStream = new BufferedReadStream(configuration, stream); - IImageInfo imageInfo = decoder.Identify(bufferedReadStream, cancellationToken); - return Task.FromResult(imageInfo); + return decoder.Identify(bufferedReadStream, cancellationToken); } catch (InvalidMemoryOperationException ex) { - InvalidImageContentException invalidImageContentException = tooLargeImageExceptionFactory(ex, decoder.Dimensions); - return Task.FromException(invalidImageContentException); - } - catch (OperationCanceledException) - { - return Task.FromCanceled(cancellationToken); - } - catch (Exception ex) - { - return Task.FromException(ex); + throw new InvalidImageContentException(decoder.Dimensions, ex); } } - /// - /// Decodes the image from the specified stream. - /// - /// The pixel format. - /// The decoder. - /// The configuration for the image. - /// The containing image data. - /// The token to monitor for cancellation requests. - /// A representing the asynchronous operation. - public static Task> DecodeAsync( - this IImageDecoderInternals decoder, - Configuration configuration, - Stream stream, - CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel => - decoder.DecodeAsync( - configuration, - stream, - DefaultLargeImageExceptionFactory, - cancellationToken); - - /// - /// Decodes the image from the specified stream. - /// - /// The pixel format. - /// The decoder. - /// The configuration for the image. - /// The containing image data. - /// Factory method to handle as . - /// The token to monitor for cancellation requests. - /// A representing the asynchronous operation. - public static Task> DecodeAsync( + public static Image Decode( this IImageDecoderInternals decoder, Configuration configuration, Stream stream, - Func largeImageExceptionFactory, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel - { - try - { - using var bufferedReadStream = new BufferedReadStream(configuration, stream); - Image image = decoder.Decode(bufferedReadStream, cancellationToken); - return Task.FromResult(image); - } - catch (InvalidMemoryOperationException ex) - { - InvalidImageContentException invalidImageContentException = largeImageExceptionFactory(ex, decoder.Dimensions); - return Task.FromException>(invalidImageContentException); - } - catch (OperationCanceledException) - { - return Task.FromCanceled>(cancellationToken); - } - catch (Exception ex) - { - return Task.FromException>(ex); - } - } - - public static IImageInfo Identify( - this IImageDecoderInternals decoder, - Configuration configuration, - Stream stream) - { - using var bufferedReadStream = new BufferedReadStream(configuration, stream); - - try - { - return decoder.Identify(bufferedReadStream, default); - } - catch (InvalidMemoryOperationException ex) - { - throw new InvalidImageContentException(decoder.Dimensions, ex); - } - } - - public static Image Decode(this IImageDecoderInternals decoder, Configuration configuration, Stream stream) - where TPixel : unmanaged, IPixel - => decoder.Decode(configuration, stream, DefaultLargeImageExceptionFactory); + => decoder.Decode(configuration, stream, DefaultLargeImageExceptionFactory, cancellationToken); public static Image Decode( this IImageDecoderInternals decoder, Configuration configuration, Stream stream, - Func largeImageExceptionFactory) + Func largeImageExceptionFactory, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { using var bufferedReadStream = new BufferedReadStream(configuration, stream); try { - return decoder.Decode(bufferedReadStream, default); + return decoder.Decode(bufferedReadStream, cancellationToken); } catch (InvalidMemoryOperationException ex) { @@ -170,6 +61,6 @@ namespace SixLabors.ImageSharp.Formats private static InvalidImageContentException DefaultLargeImageExceptionFactory( InvalidMemoryOperationException memoryOperationException, Size dimensions) => - new InvalidImageContentException(dimensions, memoryOperationException); + new(dimensions, memoryOperationException); } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/ComponentType.cs b/src/ImageSharp/Formats/Jpeg/Components/ComponentType.cs new file mode 100644 index 000000000..ff3c0539c --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/ComponentType.cs @@ -0,0 +1,12 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components +{ + internal enum ComponentType + { + Huffman = 0, + + Arithmetic = 1 + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticDecodingComponent.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticDecodingComponent.cs new file mode 100644 index 000000000..a2736900f --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticDecodingComponent.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder +{ + internal class ArithmeticDecodingComponent : JpegComponent + { + public ArithmeticDecodingComponent(MemoryAllocator memoryAllocator, JpegFrame frame, byte id, int horizontalFactor, int verticalFactor, byte quantizationTableIndex, int index) + : base(memoryAllocator, frame, id, horizontalFactor, verticalFactor, quantizationTableIndex, index) + { + } + + /// + /// Gets or sets the dc context. + /// + public int DcContext { get; set; } + + /// + /// Gets or sets the dc statistics. + /// + public ArithmeticStatistics DcStatistics { get; set; } + + /// + /// Gets or sets the ac statistics. + /// + public ArithmeticStatistics AcStatistics { get; set; } + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticDecodingTable.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticDecodingTable.cs new file mode 100644 index 000000000..6055f300d --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticDecodingTable.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder +{ + internal class ArithmeticDecodingTable + { + public ArithmeticDecodingTable(byte tableClass, byte identifier) + { + this.TableClass = tableClass; + this.Identifier = identifier; + } + + public byte TableClass { get; } + + public byte Identifier { get; } + + public byte ConditioningTableValue { get; private set; } + + public int DcL { get; private set; } + + public int DcU { get; private set; } + + public int AcKx { get; private set; } + + public void Configure(byte conditioningTableValue) + { + this.ConditioningTableValue = conditioningTableValue; + if (this.TableClass == 0) + { + this.DcL = conditioningTableValue & 0x0F; + this.DcU = conditioningTableValue >> 4; + this.AcKx = 0; + } + else + { + this.DcL = 0; + this.DcU = 0; + this.AcKx = conditioningTableValue; + } + } + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticScanDecoder.cs new file mode 100644 index 000000000..d3a5ea15b --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticScanDecoder.cs @@ -0,0 +1,1238 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using SixLabors.ImageSharp.IO; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder +{ + /// + /// Decodes a arithmetic encoded spectral scan. + /// Based on https://github.com/yigolden/JpegLibrary/blob/main/src/JpegLibrary/ScanDecoder/JpegArithmeticScanDecoder.cs + /// + internal class ArithmeticScanDecoder : IJpegScanDecoder + { + private readonly BufferedReadStream stream; + + private int c; + private int a; + private int ct; + + /// + /// instance containing decoding-related information. + /// + private JpegFrame frame; + + /// + /// Shortcut for .Components. + /// + private IJpegComponent[] components; + + /// + /// Number of component in the current scan. + /// + private int scanComponentCount; + + /// + /// The reset interval determined by RST markers. + /// + private int restartInterval; + + /// + /// How many mcu's are left to do. + /// + private int todo; + + private readonly SpectralConverter spectralConverter; + + private JpegBitReader scanBuffer; + + private ArithmeticDecodingTable[] dcDecodingTables; + + private ArithmeticDecodingTable[] acDecodingTables; + + private readonly byte[] fixedBin = { 113, 0, 0, 0 }; + + private readonly CancellationToken cancellationToken; + + private static readonly int[] ArithmeticTable = + { + Pack(0x5a1d, 1, 1, 1), + Pack(0x2586, 14, 2, 0), + Pack(0x1114, 16, 3, 0), + Pack(0x080b, 18, 4, 0), + Pack(0x03d8, 20, 5, 0), + Pack(0x01da, 23, 6, 0), + Pack(0x00e5, 25, 7, 0), + Pack(0x006f, 28, 8, 0), + Pack(0x0036, 30, 9, 0), + Pack(0x001a, 33, 10, 0), + Pack(0x000d, 35, 11, 0), + Pack(0x0006, 9, 12, 0), + Pack(0x0003, 10, 13, 0), + Pack(0x0001, 12, 13, 0), + Pack(0x5a7f, 15, 15, 1), + Pack(0x3f25, 36, 16, 0), + Pack(0x2cf2, 38, 17, 0), + Pack(0x207c, 39, 18, 0), + Pack(0x17b9, 40, 19, 0), + Pack(0x1182, 42, 20, 0), + Pack(0x0cef, 43, 21, 0), + Pack(0x09a1, 45, 22, 0), + Pack(0x072f, 46, 23, 0), + Pack(0x055c, 48, 24, 0), + Pack(0x0406, 49, 25, 0), + Pack(0x0303, 51, 26, 0), + Pack(0x0240, 52, 27, 0), + Pack(0x01b1, 54, 28, 0), + Pack(0x0144, 56, 29, 0), + Pack(0x00f5, 57, 30, 0), + Pack(0x00b7, 59, 31, 0), + Pack(0x008a, 60, 32, 0), + Pack(0x0068, 62, 33, 0), + Pack(0x004e, 63, 34, 0), + Pack(0x003b, 32, 35, 0), + Pack(0x002c, 33, 9, 0), + Pack(0x5ae1, 37, 37, 1), + Pack(0x484c, 64, 38, 0), + Pack(0x3a0d, 65, 39, 0), + Pack(0x2ef1, 67, 40, 0), + Pack(0x261f, 68, 41, 0), + Pack(0x1f33, 69, 42, 0), + Pack(0x19a8, 70, 43, 0), + Pack(0x1518, 72, 44, 0), + Pack(0x1177, 73, 45, 0), + Pack(0x0e74, 74, 46, 0), + Pack(0x0bfb, 75, 47, 0), + Pack(0x09f8, 77, 48, 0), + Pack(0x0861, 78, 49, 0), + Pack(0x0706, 79, 50, 0), + Pack(0x05cd, 48, 51, 0), + Pack(0x04de, 50, 52, 0), + Pack(0x040f, 50, 53, 0), + Pack(0x0363, 51, 54, 0), + Pack(0x02d4, 52, 55, 0), + Pack(0x025c, 53, 56, 0), + Pack(0x01f8, 54, 57, 0), + Pack(0x01a4, 55, 58, 0), + Pack(0x0160, 56, 59, 0), + Pack(0x0125, 57, 60, 0), + Pack(0x00f6, 58, 61, 0), + Pack(0x00cb, 59, 62, 0), + Pack(0x00ab, 61, 63, 0), + Pack(0x008f, 61, 32, 0), + Pack(0x5b12, 65, 65, 1), + Pack(0x4d04, 80, 66, 0), + Pack(0x412c, 81, 67, 0), + Pack(0x37d8, 82, 68, 0), + Pack(0x2fe8, 83, 69, 0), + Pack(0x293c, 84, 70, 0), + Pack(0x2379, 86, 71, 0), + Pack(0x1edf, 87, 72, 0), + Pack(0x1aa9, 87, 73, 0), + Pack(0x174e, 72, 74, 0), + Pack(0x1424, 72, 75, 0), + Pack(0x119c, 74, 76, 0), + Pack(0x0f6b, 74, 77, 0), + Pack(0x0d51, 75, 78, 0), + Pack(0x0bb6, 77, 79, 0), + Pack(0x0a40, 77, 48, 0), + Pack(0x5832, 80, 81, 1), + Pack(0x4d1c, 88, 82, 0), + Pack(0x438e, 89, 83, 0), + Pack(0x3bdd, 90, 84, 0), + Pack(0x34ee, 91, 85, 0), + Pack(0x2eae, 92, 86, 0), + Pack(0x299a, 93, 87, 0), + Pack(0x2516, 86, 71, 0), + Pack(0x5570, 88, 89, 1), + Pack(0x4ca9, 95, 90, 0), + Pack(0x44d9, 96, 91, 0), + Pack(0x3e22, 97, 92, 0), + Pack(0x3824, 99, 93, 0), + Pack(0x32b4, 99, 94, 0), + Pack(0x2e17, 93, 86, 0), + Pack(0x56a8, 95, 96, 1), + Pack(0x4f46, 101, 97, 0), + Pack(0x47e5, 102, 98, 0), + Pack(0x41cf, 103, 99, 0), + Pack(0x3c3d, 104, 100, 0), + Pack(0x375e, 99, 93, 0), + Pack(0x5231, 105, 102, 0), + Pack(0x4c0f, 106, 103, 0), + Pack(0x4639, 107, 104, 0), + Pack(0x415e, 103, 99, 0), + Pack(0x5627, 105, 106, 1), + Pack(0x50e7, 108, 107, 0), + Pack(0x4b85, 109, 103, 0), + Pack(0x5597, 110, 109, 0), + Pack(0x504f, 111, 107, 0), + Pack(0x5a10, 110, 111, 1), + Pack(0x5522, 112, 109, 0), + Pack(0x59eb, 112, 111, 1), + + // This last entry is used for fixed probability estimate of 0.5 + // as suggested in Section 10.3 Table 5 of ITU-T Rec. T.851. + Pack(0x5a1d, 113, 113, 0) + }; + + private readonly List statistics = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The input stream. + /// Spectral to pixel converter. + /// The token to monitor cancellation. + public ArithmeticScanDecoder(BufferedReadStream stream, SpectralConverter converter, CancellationToken cancellationToken) + { + this.stream = stream; + this.spectralConverter = converter; + this.cancellationToken = cancellationToken; + + this.c = 0; + this.a = 0; + this.ct = -16; // Force reading 2 initial bytes to fill C. + } + + /// + public int ResetInterval + { + set + { + this.restartInterval = value; + this.todo = value; + } + } + + /// + public int SpectralStart { get; set; } + + /// + public int SpectralEnd { get; set; } + + /// + public int SuccessiveHigh { get; set; } + + /// + public int SuccessiveLow { get; set; } + + public void InitDecodingTables(List arithmeticDecodingTables) + { + for (int i = 0; i < this.components.Length; i++) + { + var component = this.components[i] as ArithmeticDecodingComponent; + this.dcDecodingTables[i] = this.GetArithmeticTable(arithmeticDecodingTables, true, component.DcTableId); + component.DcStatistics = this.CreateOrGetStatisticsBin(true, component.DcTableId); + this.acDecodingTables[i] = this.GetArithmeticTable(arithmeticDecodingTables, false, component.AcTableId); + component.AcStatistics = this.CreateOrGetStatisticsBin(false, component.AcTableId); + } + } + + private ref byte GetFixedBinReference() => ref this.fixedBin[0]; + + /// + /// Decodes the entropy coded data. + /// + /// Component count in the current scan. + public void ParseEntropyCodedData(int scanComponentCount) + { + this.cancellationToken.ThrowIfCancellationRequested(); + + this.scanComponentCount = scanComponentCount; + + this.scanBuffer = new JpegBitReader(this.stream); + + bool fullScan = this.frame.Progressive || this.frame.MultiScan; + this.frame.AllocateComponents(fullScan); + + if (this.frame.Progressive) + { + this.ParseProgressiveData(); + } + else + { + this.ParseBaselineData(); + } + + if (this.scanBuffer.HasBadMarker()) + { + this.stream.Position = this.scanBuffer.MarkerPosition; + } + } + + /// + public void InjectFrameData(JpegFrame frame, IRawJpegData jpegData) + { + this.frame = frame; + this.components = frame.Components; + + this.dcDecodingTables = new ArithmeticDecodingTable[this.components.Length]; + this.acDecodingTables = new ArithmeticDecodingTable[this.components.Length]; + + this.spectralConverter.InjectFrameData(frame, jpegData); + } + + private ArithmeticDecodingTable GetArithmeticTable(List arithmeticDecodingTables, bool isDcTable, int identifier) + { + int tableClass = isDcTable ? 0 : 1; + + foreach (ArithmeticDecodingTable item in arithmeticDecodingTables) + { + if (item.TableClass == tableClass && item.Identifier == identifier) + { + return item; + } + } + + return null; + } + + private ArithmeticStatistics CreateOrGetStatisticsBin(bool dc, int identifier, bool reset = false) + { + foreach (ArithmeticStatistics item in this.statistics) + { + if (item.IsDcStatistics == dc && item.Identifier == identifier) + { + if (reset) + { + item.Reset(); + } + + return item; + } + } + + var statistic = new ArithmeticStatistics(dc, identifier); + this.statistics.Add(statistic); + return statistic; + } + + private void ParseBaselineData() + { + foreach (ArithmeticDecodingComponent component in this.components) + { + component.DcPredictor = 0; + component.DcContext = 0; + component.DcStatistics?.Reset(); + component.AcStatistics?.Reset(); + } + + this.Reset(); + + if (this.scanComponentCount != 1) + { + this.ParseBaselineDataInterleaved(); + this.spectralConverter.CommitConversion(); + } + else if (this.frame.ComponentCount == 1) + { + this.ParseBaselineDataSingleComponent(); + this.spectralConverter.CommitConversion(); + } + else + { + this.ParseBaselineDataNonInterleaved(); + } + } + + private void ParseProgressiveData() + { + this.CheckProgressiveData(); + + foreach (ArithmeticDecodingComponent component in this.components) + { + if (this.SpectralStart == 0 && this.SuccessiveHigh == 0) + { + component.DcPredictor = 0; + component.DcContext = 0; + component.DcStatistics?.Reset(); + } + + if (this.SpectralStart != 0) + { + component.AcStatistics?.Reset(); + } + } + + this.Reset(); + + if (this.scanComponentCount == 1) + { + this.ParseProgressiveDataNonInterleaved(); + } + else + { + this.ParseProgressiveDataInterleaved(); + } + } + + private void CheckProgressiveData() + { + // Validate successive scan parameters. + // Logic has been adapted from libjpeg. + // See Table B.3 – Scan header parameter size and values. itu-t81.pdf + bool invalid = false; + if (this.SpectralStart == 0) + { + if (this.SpectralEnd != 0) + { + invalid = true; + } + } + else + { + // Need not check Ss/Se < 0 since they came from unsigned bytes. + if (this.SpectralEnd < this.SpectralStart || this.SpectralEnd > 63) + { + invalid = true; + } + + // AC scans may have only one component. + if (this.scanComponentCount != 1) + { + invalid = true; + } + } + + if (this.SuccessiveHigh != 0) + { + // Successive approximation refinement scan: must have Al = Ah-1. + if (this.SuccessiveHigh - 1 != this.SuccessiveLow) + { + invalid = true; + } + } + + // TODO: How does this affect 12bit jpegs. + // According to libjpeg the range covers 8bit only? + if (this.SuccessiveLow > 13) + { + invalid = true; + } + + if (invalid) + { + JpegThrowHelper.ThrowBadProgressiveScan(this.SpectralStart, this.SpectralEnd, this.SuccessiveHigh, this.SuccessiveLow); + } + } + + private void ParseBaselineDataInterleaved() + { + int mcu = 0; + int mcusPerColumn = this.frame.McusPerColumn; + int mcusPerLine = this.frame.McusPerLine; + ref JpegBitReader reader = ref this.scanBuffer; + + for (int j = 0; j < mcusPerColumn; j++) + { + this.cancellationToken.ThrowIfCancellationRequested(); + + // Decode from binary to spectral. + for (int i = 0; i < mcusPerLine; i++) + { + // Scan an interleaved mcu... process components in order. + int mcuCol = mcu % mcusPerLine; + for (int k = 0; k < this.scanComponentCount; k++) + { + int order = this.frame.ComponentOrder[k]; + var component = this.components[order] as ArithmeticDecodingComponent; + + ref ArithmeticDecodingTable dcDecodingTable = ref this.dcDecodingTables[component.DcTableId]; + ref ArithmeticDecodingTable acDecodingTable = ref this.acDecodingTables[component.AcTableId]; + + int h = component.HorizontalSamplingFactor; + int v = component.VerticalSamplingFactor; + + // Scan out an mcu's worth of this component; that's just determined + // by the basic H and V specified for the component. + int mcuColMulh = mcuCol * h; + for (int y = 0; y < v; y++) + { + Span blockSpan = component.SpectralBlocks.DangerousGetRowSpan(y); + ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan); + + for (int x = 0; x < h; x++) + { + if (reader.NoData) + { + // It is very likely that some spectral data was decoded before we've encountered 'end of scan' + // so we need to decode what's left and return (or maybe throw?) + this.spectralConverter.ConvertStrideBaseline(); + return; + } + + int blockCol = mcuColMulh + x; + + this.DecodeBlockBaseline( + component, + ref Unsafe.Add(ref blockRef, (nint)(uint)blockCol), + ref acDecodingTable, + ref dcDecodingTable); + } + } + } + + // After all interleaved components, that's an interleaved MCU, + // so now count down the restart interval. + mcu++; + this.HandleRestart(); + } + + // Convert from spectral to actual pixels via given converter. + this.spectralConverter.ConvertStrideBaseline(); + } + } + + private void ParseBaselineDataSingleComponent() + { + var component = this.frame.Components[0] as ArithmeticDecodingComponent; + int mcuLines = this.frame.McusPerColumn; + int w = component.WidthInBlocks; + int h = component.SamplingFactors.Height; + ref ArithmeticDecodingTable dcDecodingTable = ref this.dcDecodingTables[component.DcTableId]; + ref ArithmeticDecodingTable acDecodingTable = ref this.acDecodingTables[component.AcTableId]; + + ref JpegBitReader reader = ref this.scanBuffer; + + for (int i = 0; i < mcuLines; i++) + { + this.cancellationToken.ThrowIfCancellationRequested(); + + // Decode from binary to spectral. + for (int j = 0; j < h; j++) + { + Span blockSpan = component.SpectralBlocks.DangerousGetRowSpan(j); + ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan); + + for (int k = 0; k < w; k++) + { + if (reader.NoData) + { + // It is very likely that some spectral data was decoded before we've encountered 'end of scan' + // so we need to decode what's left and return (or maybe throw?) + this.spectralConverter.ConvertStrideBaseline(); + return; + } + + this.DecodeBlockBaseline( + component, + ref Unsafe.Add(ref blockRef, (nint)(uint)k), + ref acDecodingTable, + ref dcDecodingTable); + + this.HandleRestart(); + } + } + + // Convert from spectral to actual pixels via given converter. + this.spectralConverter.ConvertStrideBaseline(); + } + } + + private void ParseBaselineDataNonInterleaved() + { + var component = (ArithmeticDecodingComponent)this.components[this.frame.ComponentOrder[0]]; + ref JpegBitReader reader = ref this.scanBuffer; + + int w = component.WidthInBlocks; + int h = component.HeightInBlocks; + + ref ArithmeticDecodingTable dcDecodingTable = ref this.dcDecodingTables[component.DcTableId]; + ref ArithmeticDecodingTable acDecodingTable = ref this.acDecodingTables[component.AcTableId]; + + for (int j = 0; j < h; j++) + { + this.cancellationToken.ThrowIfCancellationRequested(); + Span blockSpan = component.SpectralBlocks.DangerousGetRowSpan(j); + ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan); + + for (int i = 0; i < w; i++) + { + if (reader.NoData) + { + return; + } + + this.DecodeBlockBaseline( + component, + ref Unsafe.Add(ref blockRef, (nint)(uint)i), + ref acDecodingTable, + ref dcDecodingTable); + + this.HandleRestart(); + } + } + } + + private void ParseProgressiveDataInterleaved() + { + int mcu = 0; + int mcusPerColumn = this.frame.McusPerColumn; + int mcusPerLine = this.frame.McusPerLine; + ref JpegBitReader reader = ref this.scanBuffer; + + for (int j = 0; j < mcusPerColumn; j++) + { + for (int i = 0; i < mcusPerLine; i++) + { + // Scan an interleaved mcu... process components in order. + int mcuRow = Math.DivRem(mcu, mcusPerLine, out int mcuCol); + for (int k = 0; k < this.scanComponentCount; k++) + { + int order = this.frame.ComponentOrder[k]; + var component = this.components[order] as ArithmeticDecodingComponent; + ref ArithmeticDecodingTable dcDecodingTable = ref this.dcDecodingTables[component.DcTableId]; + + int h = component.HorizontalSamplingFactor; + int v = component.VerticalSamplingFactor; + + // Scan out an mcu's worth of this component; that's just determined + // by the basic H and V specified for the component. + int mcuColMulh = mcuCol * h; + for (int y = 0; y < v; y++) + { + int blockRow = (mcuRow * v) + y; + Span blockSpan = component.SpectralBlocks.DangerousGetRowSpan(blockRow); + ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan); + + for (int x = 0; x < h; x++) + { + if (reader.NoData) + { + return; + } + + int blockCol = mcuColMulh + x; + + this.DecodeBlockProgressiveDc( + component, + ref Unsafe.Add(ref blockRef, (nint)(uint)blockCol), + ref dcDecodingTable); + } + } + } + + // After all interleaved components, that's an interleaved MCU, + // so now count down the restart interval. + mcu++; + this.HandleRestart(); + } + } + } + + private void ParseProgressiveDataNonInterleaved() + { + var component = this.components[this.frame.ComponentOrder[0]] as ArithmeticDecodingComponent; + ref JpegBitReader reader = ref this.scanBuffer; + + int w = component.WidthInBlocks; + int h = component.HeightInBlocks; + + if (this.SpectralStart == 0) + { + ref ArithmeticDecodingTable dcDecodingTable = ref this.dcDecodingTables[component.DcTableId]; + + for (int j = 0; j < h; j++) + { + this.cancellationToken.ThrowIfCancellationRequested(); + + Span blockSpan = component.SpectralBlocks.DangerousGetRowSpan(j); + ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan); + + for (int i = 0; i < w; i++) + { + if (reader.NoData) + { + return; + } + + this.DecodeBlockProgressiveDc( + component, + ref Unsafe.Add(ref blockRef, (nint)(uint)i), + ref dcDecodingTable); + + this.HandleRestart(); + } + } + } + else + { + ref ArithmeticDecodingTable acDecodingTable = ref this.acDecodingTables[component.AcTableId]; + + for (int j = 0; j < h; j++) + { + this.cancellationToken.ThrowIfCancellationRequested(); + + Span blockSpan = component.SpectralBlocks.DangerousGetRowSpan(j); + ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan); + + for (int i = 0; i < w; i++) + { + if (reader.NoData) + { + return; + } + + this.DecodeBlockProgressiveAc( + component, + ref Unsafe.Add(ref blockRef, (nint)(uint)i), + ref acDecodingTable); + + this.HandleRestart(); + } + } + } + } + + private void DecodeBlockProgressiveDc(ArithmeticDecodingComponent component, ref Block8x8 block, ref ArithmeticDecodingTable dcTable) + { + if (dcTable == null) + { + JpegThrowHelper.ThrowInvalidImageContentException("DC table is missing"); + } + + ref JpegBitReader reader = ref this.scanBuffer; + ref short blockDataRef = ref Unsafe.As(ref block); + + if (this.SuccessiveHigh == 0) + { + // First scan + // Sections F.2.4.1 & F.1.4.4.1: Decoding of DC coefficients. + + // Table F.4: Point to statistics bin S0 for DC coefficient coding. + ref byte st = ref Unsafe.Add(ref component.DcStatistics.GetReference(), component.DcContext); + + // Figure F.19: Decode_DC_DIFF + if (this.DecodeBinaryDecision(ref reader, ref st) == 0) + { + component.DcContext = 0; + } + else + { + // Figure F.21: Decoding nonzero value v. + // Figure F.22: Decoding the sign of v. + int sign = this.DecodeBinaryDecision(ref reader, ref Unsafe.Add(ref st, 1)); + st = ref Unsafe.Add(ref st, (nint)(uint)(2 + sign)); + + // Figure F.23: Decoding the magnitude category of v. + int m = this.DecodeBinaryDecision(ref reader, ref st); + if (m != 0) + { + st = ref component.DcStatistics.GetReference(20); + while (this.DecodeBinaryDecision(ref reader, ref st) != 0) + { + if ((m <<= 1) == 0x8000) + { + JpegThrowHelper.ThrowInvalidImageContentException("Invalid arithmetic code."); + } + + st = ref Unsafe.Add(ref st, 1); + } + } + + // Section F.1.4.4.1.2: Establish dc_context conditioning category. + if (m < (int)((1L << dcTable.DcL) >> 1)) + { + component.DcContext = 0; // Zero diff category. + } + else if (m > (int)((1L << dcTable.DcU) >> 1)) + { + component.DcContext = 12 + (sign * 4); // Large diff category. + } + else + { + component.DcContext = 4 + (sign * 4); // Small diff category. + } + + int v = m; + + // Figure F.24: Decoding the magnitude bit pattern of v. + st = ref Unsafe.Add(ref st, 14); + while ((m >>= 1) != 0) + { + if (this.DecodeBinaryDecision(ref reader, ref st) != 0) + { + v |= m; + } + } + + v += 1; + if (sign != 0) + { + v = -v; + } + + component.DcPredictor = (short)(component.DcPredictor + v); + } + + blockDataRef = (short)(component.DcPredictor << this.SuccessiveLow); + } + else + { + // Refinement scan. + ref byte st = ref this.GetFixedBinReference(); + + blockDataRef |= (short)(this.DecodeBinaryDecision(ref reader, ref st) << this.SuccessiveLow); + } + } + + private void DecodeBlockProgressiveAc(ArithmeticDecodingComponent component, ref Block8x8 block, ref ArithmeticDecodingTable acTable) + { + ref JpegBitReader reader = ref this.scanBuffer; + ref short blockDataRef = ref Unsafe.As(ref block); + + ArithmeticStatistics acStatistics = component.AcStatistics; + if (acStatistics == null || acTable == null) + { + JpegThrowHelper.ThrowInvalidImageContentException("AC table is missing"); + } + + if (this.SuccessiveHigh == 0) + { + // Sections F.2.4.2 & F.1.4.4.2: Decoding of AC coefficients. + + // Figure F.20: Decode_AC_coefficients. + int start = this.SpectralStart; + int end = this.SpectralEnd; + int low = this.SuccessiveLow; + + for (int k = start; k <= end; k++) + { + ref byte st = ref acStatistics.GetReference(3 * (k - 1)); + if (this.DecodeBinaryDecision(ref reader, ref st) != 0) + { + break; + } + + while (this.DecodeBinaryDecision(ref reader, ref Unsafe.Add(ref st, 1)) == 0) + { + st = ref Unsafe.Add(ref st, 3); + k++; + if (k > 63) + { + JpegThrowHelper.ThrowInvalidImageContentException("Invalid arithmetic code."); + } + } + + // Figure F.21: Decoding nonzero value v. + // Figure F.22: Decoding the sign of v. + int sign = this.DecodeBinaryDecision(ref reader, ref this.GetFixedBinReference()); + st = ref Unsafe.Add(ref st, 2); + + // Figure F.23: Decoding the magnitude category of v. + int m = this.DecodeBinaryDecision(ref reader, ref st); + if (m != 0) + { + if (this.DecodeBinaryDecision(ref reader, ref st) != 0) + { + m <<= 1; + st = ref acStatistics.GetReference(k <= acTable.AcKx ? 189 : 217); + while (this.DecodeBinaryDecision(ref reader, ref st) != 0) + { + if ((m <<= 1) == 0x8000) + { + JpegThrowHelper.ThrowInvalidImageContentException("Invalid arithmetic code."); + } + + st = ref Unsafe.Add(ref st, 1); + } + } + } + + int v = m; + + // Figure F.24: Decoding the magnitude bit pattern of v. + st = ref Unsafe.Add(ref st, 14); + while ((m >>= 1) != 0) + { + if (this.DecodeBinaryDecision(ref reader, ref st) != 0) + { + v |= m; + } + } + + v += 1; + if (sign != 0) + { + v = -v; + } + + Unsafe.Add(ref blockDataRef, ZigZag.TransposingOrder[k]) = (short)(v << low); + } + } + else + { + // Refinement scan. + this.ReadBlockProgressiveAcRefined(acStatistics, ref blockDataRef); + } + } + + private void ReadBlockProgressiveAcRefined(ArithmeticStatistics acStatistics, ref short blockDataRef) + { + ref JpegBitReader reader = ref this.scanBuffer; + int start = this.SpectralStart; + int end = this.SpectralEnd; + + int p1 = 1 << this.SuccessiveLow; + int m1 = -1 << this.SuccessiveLow; + + // Establish EOBx (previous stage end-of-block) index. + int kex = end; + for (; kex > 0; kex--) + { + if (Unsafe.Add(ref blockDataRef, ZigZag.TransposingOrder[kex]) != 0) + { + break; + } + } + + for (int k = start; k <= end; k++) + { + ref byte st = ref acStatistics.GetReference(3 * (k - 1)); + if (k > kex) + { + if (this.DecodeBinaryDecision(ref reader, ref st) != 0) + { + break; + } + } + + while (true) + { + ref short coef = ref Unsafe.Add(ref blockDataRef, ZigZag.TransposingOrder[k]); + if (coef != 0) + { + if (this.DecodeBinaryDecision(ref reader, ref Unsafe.Add(ref st, 2)) != 0) + { + coef = (short)(coef + (coef < 0 ? m1 : p1)); + } + + break; + } + + if (this.DecodeBinaryDecision(ref reader, ref Unsafe.Add(ref st, 1)) != 0) + { + bool flag = this.DecodeBinaryDecision(ref reader, ref this.GetFixedBinReference()) != 0; + coef = (short)(coef + (flag ? m1 : p1)); + + break; + } + + st = ref Unsafe.Add(ref st, 3); + k++; + if (k > end) + { + JpegThrowHelper.ThrowInvalidImageContentException("Invalid arithmetic code."); + } + } + } + } + + private void DecodeBlockBaseline( + ArithmeticDecodingComponent component, + ref Block8x8 destinationBlock, + ref ArithmeticDecodingTable acTable, + ref ArithmeticDecodingTable dcTable) + { + if (acTable is null) + { + JpegThrowHelper.ThrowInvalidImageContentException("AC table is missing."); + } + + if (dcTable is null) + { + JpegThrowHelper.ThrowInvalidImageContentException("DC table is missing."); + } + + ref JpegBitReader reader = ref this.scanBuffer; + ref short destinationRef = ref Unsafe.As(ref destinationBlock); + + // Sections F.2.4.1 & F.1.4.4.1: Decoding of DC coefficients. + + // Table F.4: Point to statistics bin S0 for DC coefficient coding. + ref byte st = ref Unsafe.Add(ref component.DcStatistics.GetReference(), component.DcContext); + + /* Figure F.19: Decode_DC_DIFF */ + if (this.DecodeBinaryDecision(ref reader, ref st) == 0) + { + component.DcContext = 0; + } + else + { + // Figure F.21: Decoding nonzero value v + // Figure F.22: Decoding the sign of v + int sign = this.DecodeBinaryDecision(ref reader, ref Unsafe.Add(ref st, 1)); + st = ref Unsafe.Add(ref st, (nint)(uint)(2 + sign)); + + // Figure F.23: Decoding the magnitude category of v. + int m = this.DecodeBinaryDecision(ref reader, ref st); + if (m != 0) + { + // Table F.4: X1 = 20 + st = ref component.DcStatistics.GetReference(20); + while (this.DecodeBinaryDecision(ref reader, ref st) != 0) + { + if ((m <<= 1) == 0x8000) + { + JpegThrowHelper.ThrowInvalidImageContentException("Invalid arithmetic code."); + } + + st = ref Unsafe.Add(ref st, 1); + } + } + + // Section F.1.4.4.1.2: Establish dc_context conditioning category. + if (m < (int)((1L << dcTable.DcL) >> 1)) + { + component.DcContext = 0; // zero diff category + } + else if (m > (int)((1L << dcTable.DcU) >> 1)) + { + component.DcContext = 12 + (sign * 4); // large diff category + } + else + { + component.DcContext = 4 + (sign * 4); // small diff category + } + + int v = m; + + // Figure F.24: Decoding the magnitude bit pattern of v. + st = ref Unsafe.Add(ref st, 14); + while ((m >>= 1) != 0) + { + if (this.DecodeBinaryDecision(ref reader, ref st) != 0) + { + v |= m; + } + } + + v += 1; + if (sign != 0) + { + v = -v; + } + + component.DcPredictor = (short)(component.DcPredictor + v); + } + + destinationRef = (short)component.DcPredictor; + + // Sections F.2.4.2 & F.1.4.4.2: Decoding of AC coefficients. + ArithmeticStatistics acStatistics = component.AcStatistics; + + for (int k = 1; k <= 63; k++) + { + st = ref acStatistics.GetReference(3 * (k - 1)); + if (this.DecodeBinaryDecision(ref reader, ref st) != 0) + { + // EOB flag. + break; + } + + while (this.DecodeBinaryDecision(ref reader, ref Unsafe.Add(ref st, 1)) == 0) + { + st = ref Unsafe.Add(ref st, 3); + k++; + if (k > 63) + { + JpegThrowHelper.ThrowInvalidImageContentException("Invalid arithmetic code."); + } + } + + // Figure F.21: Decoding nonzero value v. + // Figure F.22: Decoding the sign of v. + int sign = this.DecodeBinaryDecision(ref reader, ref this.GetFixedBinReference()); + st = ref Unsafe.Add(ref st, 2); + + // Figure F.23: Decoding the magnitude category of v. + int m = this.DecodeBinaryDecision(ref reader, ref st); + if (m != 0) + { + if (this.DecodeBinaryDecision(ref reader, ref st) != 0) + { + m <<= 1; + st = ref acStatistics.GetReference(k <= acTable.AcKx ? 189 : 217); + while (this.DecodeBinaryDecision(ref reader, ref st) != 0) + { + if ((m <<= 1) == 0x8000) + { + JpegThrowHelper.ThrowInvalidImageContentException("Invalid arithmetic code."); + } + + st = ref Unsafe.Add(ref st, 1); + } + } + } + + int v = m; + + // Figure F.24: Decoding the magnitude bit pattern of v. + st = ref Unsafe.Add(ref st, 14); + while ((m >>= 1) != 0) + { + if (this.DecodeBinaryDecision(ref reader, ref st) != 0) + { + v |= m; + } + } + + v += 1; + if (sign != 0) + { + v = -v; + } + + Unsafe.Add(ref destinationRef, ZigZag.TransposingOrder[k]) = (short)v; + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private bool HandleRestart() + { + if (this.restartInterval > 0 && (--this.todo) == 0) + { + if (this.scanBuffer.Marker == JpegConstants.Markers.XFF) + { + if (!this.scanBuffer.FindNextMarker()) + { + return false; + } + } + + this.todo = this.restartInterval; + + foreach (ArithmeticDecodingComponent component in this.components) + { + component.DcPredictor = 0; + component.DcContext = 0; + component.DcStatistics?.Reset(); + component.AcStatistics?.Reset(); + } + + this.Reset(); + + if (this.scanBuffer.HasRestartMarker()) + { + this.Reset(); + return true; + } + + if (this.scanBuffer.HasBadMarker()) + { + this.stream.Position = this.scanBuffer.MarkerPosition; + this.Reset(); + return true; + } + } + + return false; + } + + [MethodImpl(InliningOptions.ShortMethod)] + private void Reset() + { + for (int i = 0; i < this.components.Length; i++) + { + var component = this.components[i] as ArithmeticDecodingComponent; + component.DcPredictor = 0; + } + + this.c = 0; + this.a = 0; + this.ct = -16; // Force reading 2 initial bytes to fill C. + + this.scanBuffer.Reset(); + } + + private int DecodeBinaryDecision(ref JpegBitReader reader, ref byte st) + { + // Renormalization & data input per section D.2.6 + while (this.a < 0x8000) + { + if (--this.ct < 0) + { + // Need to fetch next data byte. + reader.CheckBits(); + int data = reader.GetBits(8); + + // Insert data into C register. + this.c = (this.c << 8) | data; + + // Update bit shift counter. + if ((this.ct += 8) < 0) + { + // Need more initial bytes. + if (++this.ct == 0) + { + // Got 2 initial bytes -> re-init A and exit loop + this.a = 0x8000; // e->a = 0x10000L after loop exit + } + } + } + + this.a <<= 1; + } + + // Fetch values from our compact representation of Table D.3(D.2): + // Qe values and probability estimation state machine + int sv = st; + int qe = ArithmeticTable[sv & 0x7f]; + byte nl = (byte)qe; + qe >>= 8; // Next_Index_LPS + Switch_MPS + byte nm = (byte)qe; + qe >>= 8; // Next_Index_MPS + + // Decode & estimation procedures per sections D.2.4 & D.2.5 + int temp = this.a - qe; + this.a = temp; + temp <<= this.ct; + if (this.c >= temp) + { + this.c -= temp; + + // Conditional LPS (less probable symbol) exchange + if (this.a < qe) + { + this.a = qe; + st = (byte)((sv & 0x80) ^ nm); // Estimate_after_MPS + } + else + { + this.a = qe; + st = (byte)((sv & 0x80) ^ nl); // Estimate_after_LPS + sv ^= 0x80; // Exchange LPS/MPS + } + } + else if (this.a < 0x8000) + { + // Conditional MPS (more probable symbol) exchange + if (this.a < qe) + { + st = (byte)((sv & 0x80) ^ nl); // Estimate_after_LPS + sv ^= 0x80; // Exchange LPS/MPS + } + else + { + st = (byte)((sv & 0x80) ^ nm); // Estimate_after_MPS + } + } + + return sv >> 7; + } + + // The following function specifies the packing of the four components + // into the compact INT32 representation. + // Note that this formula must match the actual arithmetic encoder and decoder implementation. The implementation has to be changed + // if this formula is changed. + // The current organization is leaned on Markus Kuhn's JBIG implementation (jbig_tab.c). + [MethodImpl(InliningOptions.ShortMethod)] + private static int Pack(int a, int b, int c, int d) + => (a << 16) | (c << 8) | (d << 7) | b; + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticStatistics.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticStatistics.cs new file mode 100644 index 000000000..c84831b3a --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticStatistics.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder +{ + internal class ArithmeticStatistics + { + private readonly byte[] statistics; + + public ArithmeticStatistics(bool dc, int identifier) + { + this.IsDcStatistics = dc; + this.Identifier = identifier; + this.statistics = dc ? new byte[64] : new byte[256]; + } + + public bool IsDcStatistics { get; private set; } + + public int Identifier { get; private set; } + + public ref byte GetReference() => ref this.statistics[0]; + + public ref byte GetReference(int offset) => ref this.statistics[offset]; + + public void Reset() => this.statistics.AsSpan().Clear(); + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs index 2ae3ae86b..da2d5da65 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs @@ -14,7 +14,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// Originally ported from /// with additional fixes for both performance and common encoding errors. /// - internal class HuffmanScanDecoder + internal class HuffmanScanDecoder : IJpegScanDecoder { private readonly BufferedReadStream stream; @@ -26,12 +26,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// /// Shortcut for .Components. /// - private JpegComponent[] components; + private IJpegComponent[] components; /// /// Number of component in the current scan. /// - private int componentsCount; + private int scanComponentCount; /// /// The reset interval determined by RST markers. @@ -54,11 +54,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder private readonly HuffmanTable[] dcHuffmanTables; /// - /// The AC Huffman tables + /// The AC Huffman tables. /// private readonly HuffmanTable[] acHuffmanTables; - private HuffmanScanBuffer scanBuffer; + private JpegBitReader scanBuffer; private readonly SpectralConverter spectralConverter; @@ -112,13 +112,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// /// Decodes the entropy coded data. /// - public void ParseEntropyCodedData(int componentCount) + /// Component count in the current scan. + public void ParseEntropyCodedData(int scanComponentCount) { this.cancellationToken.ThrowIfCancellationRequested(); - this.componentsCount = componentCount; + this.scanComponentCount = scanComponentCount; - this.scanBuffer = new HuffmanScanBuffer(this.stream); + this.scanBuffer = new JpegBitReader(this.stream); bool fullScan = this.frame.Progressive || this.frame.MultiScan; this.frame.AllocateComponents(fullScan); @@ -138,6 +139,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder } } + /// public void InjectFrameData(JpegFrame frame, IRawJpegData jpegData) { this.frame = frame; @@ -148,11 +150,16 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder private void ParseBaselineData() { - if (this.componentsCount == this.frame.ComponentCount) + if (this.scanComponentCount != 1) { this.ParseBaselineDataInterleaved(); this.spectralConverter.CommitConversion(); } + else if (this.frame.ComponentCount == 1) + { + this.ParseBaselineDataSingleComponent(); + this.spectralConverter.CommitConversion(); + } else { this.ParseBaselineDataNonInterleaved(); @@ -161,11 +168,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder private void ParseBaselineDataInterleaved() { - // Interleaved int mcu = 0; int mcusPerColumn = this.frame.McusPerColumn; int mcusPerLine = this.frame.McusPerLine; - ref HuffmanScanBuffer buffer = ref this.scanBuffer; + ref JpegBitReader buffer = ref this.scanBuffer; for (int j = 0; j < mcusPerColumn; j++) { @@ -176,13 +182,13 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { // Scan an interleaved mcu... process components in order int mcuCol = mcu % mcusPerLine; - for (int k = 0; k < this.componentsCount; k++) + for (int k = 0; k < this.scanComponentCount; k++) { int order = this.frame.ComponentOrder[k]; - JpegComponent component = this.components[order]; + var component = this.components[order] as JpegComponent; - ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DCHuffmanTableId]; - ref HuffmanTable acHuffmanTable = ref this.acHuffmanTables[component.ACHuffmanTableId]; + ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DcTableId]; + ref HuffmanTable acHuffmanTable = ref this.acHuffmanTables[component.AcTableId]; int h = component.HorizontalSamplingFactor; int v = component.VerticalSamplingFactor; @@ -198,7 +204,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { if (buffer.NoData) { - // It is very likely that some spectral data was decoded before we encountered EOI marker + // It is very likely that some spectral data was decoded before we've encountered 'end of scan' // so we need to decode what's left and return (or maybe throw?) this.spectralConverter.ConvertStrideBaseline(); return; @@ -221,21 +227,21 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder this.HandleRestart(); } - // convert from spectral to actual pixels via given converter + // Convert from spectral to actual pixels via given converter this.spectralConverter.ConvertStrideBaseline(); } } private void ParseBaselineDataNonInterleaved() { - JpegComponent component = this.components[this.frame.ComponentOrder[0]]; - ref HuffmanScanBuffer buffer = ref this.scanBuffer; + var component = this.components[this.frame.ComponentOrder[0]] as JpegComponent; + ref JpegBitReader buffer = ref this.scanBuffer; int w = component.WidthInBlocks; int h = component.HeightInBlocks; - ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DCHuffmanTableId]; - ref HuffmanTable acHuffmanTable = ref this.acHuffmanTables[component.ACHuffmanTableId]; + ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DcTableId]; + ref HuffmanTable acHuffmanTable = ref this.acHuffmanTables[component.AcTableId]; for (int j = 0; j < h; j++) { @@ -261,6 +267,52 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder } } + private void ParseBaselineDataSingleComponent() + { + var component = this.frame.Components[0] as JpegComponent; + int mcuLines = this.frame.McusPerColumn; + int w = component.WidthInBlocks; + int h = component.SamplingFactors.Height; + ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DcTableId]; + ref HuffmanTable acHuffmanTable = ref this.acHuffmanTables[component.AcTableId]; + + ref JpegBitReader buffer = ref this.scanBuffer; + + for (int i = 0; i < mcuLines; i++) + { + this.cancellationToken.ThrowIfCancellationRequested(); + + // decode from binary to spectral + for (int j = 0; j < h; j++) + { + Span blockSpan = component.SpectralBlocks.DangerousGetRowSpan(j); + ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan); + + for (int k = 0; k < w; k++) + { + if (buffer.NoData) + { + // It is very likely that some spectral data was decoded before we've encountered 'end of scan' + // so we need to decode what's left and return (or maybe throw?) + this.spectralConverter.ConvertStrideBaseline(); + return; + } + + this.DecodeBlockBaseline( + component, + ref Unsafe.Add(ref blockRef, k), + ref dcHuffmanTable, + ref acHuffmanTable); + + this.HandleRestart(); + } + } + + // Convert from spectral to actual pixels via given converter + this.spectralConverter.ConvertStrideBaseline(); + } + } + private void CheckProgressiveData() { // Validate successive scan parameters. @@ -283,7 +335,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder } // AC scans may have only one component. - if (this.componentsCount != 1) + if (this.scanComponentCount != 1) { invalid = true; } @@ -315,7 +367,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { this.CheckProgressiveData(); - if (this.componentsCount == 1) + if (this.scanComponentCount == 1) { this.ParseProgressiveDataNonInterleaved(); } @@ -331,7 +383,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder int mcu = 0; int mcusPerColumn = this.frame.McusPerColumn; int mcusPerLine = this.frame.McusPerLine; - ref HuffmanScanBuffer buffer = ref this.scanBuffer; + ref JpegBitReader buffer = ref this.scanBuffer; for (int j = 0; j < mcusPerColumn; j++) { @@ -340,11 +392,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder // Scan an interleaved mcu... process components in order int mcuRow = mcu / mcusPerLine; int mcuCol = mcu % mcusPerLine; - for (int k = 0; k < this.componentsCount; k++) + for (int k = 0; k < this.scanComponentCount; k++) { int order = this.frame.ComponentOrder[k]; - JpegComponent component = this.components[order]; - ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DCHuffmanTableId]; + var component = this.components[order] as JpegComponent; + ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DcTableId]; int h = component.HorizontalSamplingFactor; int v = component.VerticalSamplingFactor; @@ -384,15 +436,15 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder private void ParseProgressiveDataNonInterleaved() { - JpegComponent component = this.components[this.frame.ComponentOrder[0]]; - ref HuffmanScanBuffer buffer = ref this.scanBuffer; + var component = this.components[this.frame.ComponentOrder[0]] as JpegComponent; + ref JpegBitReader buffer = ref this.scanBuffer; int w = component.WidthInBlocks; int h = component.HeightInBlocks; if (this.SpectralStart == 0) { - ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DCHuffmanTableId]; + ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DcTableId]; for (int j = 0; j < h; j++) { @@ -419,7 +471,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder } else { - ref HuffmanTable acHuffmanTable = ref this.acHuffmanTables[component.ACHuffmanTableId]; + ref HuffmanTable acHuffmanTable = ref this.acHuffmanTables[component.AcTableId]; for (int j = 0; j < h; j++) { @@ -452,7 +504,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder ref HuffmanTable acTable) { ref short blockDataRef = ref Unsafe.As(ref block); - ref HuffmanScanBuffer buffer = ref this.scanBuffer; + ref JpegBitReader buffer = ref this.scanBuffer; // DC int t = buffer.DecodeHuffman(ref dcTable); @@ -494,7 +546,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder private void DecodeBlockProgressiveDC(JpegComponent component, ref Block8x8 block, ref HuffmanTable dcTable) { ref short blockDataRef = ref Unsafe.As(ref block); - ref HuffmanScanBuffer buffer = ref this.scanBuffer; + ref JpegBitReader buffer = ref this.scanBuffer; if (this.SuccessiveHigh == 0) { @@ -530,7 +582,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder return; } - ref HuffmanScanBuffer buffer = ref this.scanBuffer; + ref JpegBitReader buffer = ref this.scanBuffer; int start = this.SpectralStart; int end = this.SpectralEnd; int low = this.SuccessiveLow; @@ -575,7 +627,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder private void DecodeBlockProgressiveACRefined(ref short blockDataRef, ref HuffmanTable acTable) { // Refinement scan for these AC coefficients - ref HuffmanScanBuffer buffer = ref this.scanBuffer; + ref JpegBitReader buffer = ref this.scanBuffer; int start = this.SpectralStart; int end = this.SpectralEnd; diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs index bee5e0229..79713388b 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs @@ -25,7 +25,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// /// Contains the largest code of length k (0 if none). MaxCode[17] is a sentinel to - /// ensure terminates. + /// ensure terminates. /// public fixed ulong MaxCode[18]; diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/IJpegComponent.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/IJpegComponent.cs index 54077339d..adab8c2ec 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/IJpegComponent.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/IJpegComponent.cs @@ -10,6 +10,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// internal interface IJpegComponent { + /// + /// Gets the component id. + /// + byte Id { get; } + /// /// Gets the component's position in the components array. /// @@ -25,6 +30,16 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// Size SamplingFactors { get; } + /// + /// Gets the horizontal sampling factor. + /// + int HorizontalSamplingFactor { get; } + + /// + /// Gets the vertical sampling factor. + /// + int VerticalSamplingFactor { get; } + /// /// Gets the divisors needed to apply when calculating colors. /// @@ -44,5 +59,38 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// We need to apply IDCT and dequantization to transform them into color-space blocks. /// Buffer2D SpectralBlocks { get; } + + /// + /// Gets or sets DC coefficient predictor. + /// + int DcPredictor { get; set; } + + /// + /// Gets or sets the index for the DC table. + /// + int DcTableId { get; set; } + + /// + /// Gets or sets the index for the AC table. + /// + int AcTableId { get; set; } + + /// + /// Initializes component for future buffers initialization. + /// + /// Maximal horizontal subsampling factor among all the components. + /// Maximal vertical subsampling factor among all the components. + void Init(int maxSubFactorH, int maxSubFactorV); + + /// + /// Allocates the spectral blocks. + /// + /// if set to true, use the full height of a block, otherwise use the vertical sampling factor. + void AllocateSpectral(bool fullScan); + + /// + /// Releases resources. + /// + void Dispose(); } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/IJpegScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/IJpegScanDecoder.cs new file mode 100644 index 000000000..73ca9f08b --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/IJpegScanDecoder.cs @@ -0,0 +1,49 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder +{ + /// + /// Interface for a JPEG scan decoder. + /// + internal interface IJpegScanDecoder + { + /// + /// Sets the reset interval. + /// + int ResetInterval { set; } + + /// + /// Gets or sets the spectral selection start. + /// + int SpectralStart { get; set; } + + /// + /// Gets or sets the spectral selection end. + /// + int SpectralEnd { get; set; } + + /// + /// Gets or sets the successive approximation high bit end. + /// + int SuccessiveHigh { get; set; } + + /// + /// Gets or sets the successive approximation low bit end. + /// + int SuccessiveLow { get; set; } + + /// + /// Decodes the entropy coded data. + /// + /// Component count in the current scan. + void ParseEntropyCodedData(int scanComponentCount); + + /// + /// Sets the JpegFrame and its components and injects the frame data into the spectral converter. + /// + /// The frame. + /// The raw JPEG data. + void InjectFrameData(JpegFrame frame, IRawJpegData jpegData); + } +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/IRawJpegData.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/IRawJpegData.cs index 33815e539..dd7ca4e7f 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/IRawJpegData.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/IRawJpegData.cs @@ -18,7 +18,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// /// Gets the components. /// - IJpegComponent[] Components { get; } + JpegComponent[] Components { get; } /// /// Gets the quantization tables, in natural order. diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanBuffer.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBitReader.cs similarity index 98% rename from src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanBuffer.cs rename to src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBitReader.cs index 3664cb4eb..84013319e 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanBuffer.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegBitReader.cs @@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// /// Used to buffer and track the bits read from the Huffman entropy encoded data. /// - internal struct HuffmanScanBuffer + internal struct JpegBitReader { private readonly BufferedReadStream stream; @@ -22,7 +22,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder // Whether there is no more good data to pull from the stream for the current mcu. private bool badData; - public HuffmanScanBuffer(BufferedReadStream stream) + public JpegBitReader(BufferedReadStream stream) { this.stream = stream; this.data = 0ul; diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponent.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponent.cs index 3804e1c6c..5b0e87785 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponent.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponent.cs @@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// /// Represents a single frame component. /// - internal sealed class JpegComponent : IDisposable, IJpegComponent + internal class JpegComponent : IDisposable, IJpegComponent { private readonly MemoryAllocator memoryAllocator; @@ -78,12 +78,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// /// Gets or sets the index for the DC Huffman table. /// - public int DCHuffmanTableId { get; set; } + public int DcTableId { get; set; } /// /// Gets or sets the index for the AC Huffman table. /// - public int ACHuffmanTableId { get; set; } + public int AcTableId { get; set; } public JpegFrame Frame { get; } @@ -119,11 +119,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder } } + /// public void AllocateSpectral(bool fullScan) { if (this.SpectralBlocks != null) { - // this method will be called each scan marker so we need to allocate only once + // This method will be called each scan marker so we need to allocate only once. return; } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs index c3bf1cbdd..6173c4fbf 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs @@ -103,7 +103,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder // To be "more accurate", we need to emulate this by rounding! workspaceBlock.NormalizeColorsAndRoundInPlace(maximumValue); - // Write to color buffer acording to sampling factors + // Write to color buffer according to sampling factors int xColorBufferStart = xBlock * this.blockAreaSize.Width; workspaceBlock.ScaledCopyTo( ref colorBufferRow[xColorBufferStart], diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFrame.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFrame.cs index fc109be26..db1febd39 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFrame.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegFrame.cs @@ -6,14 +6,14 @@ using System; namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { /// - /// Represent a single jpeg frame + /// Represent a single jpeg frame. /// internal sealed class JpegFrame : IDisposable { public JpegFrame(JpegFileMarker sofMarker, byte precision, int width, int height, byte componentCount) { - this.Extended = sofMarker.Marker == JpegConstants.Markers.SOF1; - this.Progressive = sofMarker.Marker == JpegConstants.Markers.SOF2; + this.Extended = sofMarker.Marker is JpegConstants.Markers.SOF1 or JpegConstants.Markers.SOF9; + this.Progressive = sofMarker.Marker is JpegConstants.Markers.SOF2 or JpegConstants.Markers.SOF10; this.Precision = precision; this.MaxColorChannelValue = MathF.Pow(2, precision) - 1; @@ -65,7 +65,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// /// Gets the pixel size of the image. /// - public Size PixelSize => new Size(this.PixelWidth, this.PixelHeight); + public Size PixelSize => new(this.PixelWidth, this.PixelHeight); /// /// Gets the number of components within a frame. @@ -101,7 +101,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// /// Gets the mcu size of the image. /// - public Size McuSize => new Size(this.McusPerLine, this.McusPerColumn); + public Size McuSize => new(this.McusPerLine, this.McusPerColumn); /// /// Gets the color depth, in number of bits per pixel. @@ -134,7 +134,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder for (int i = 0; i < this.ComponentCount; i++) { - JpegComponent component = this.Components[i]; + IJpegComponent component = this.Components[i]; component.Init(maxSubFactorH, maxSubFactorV); } } @@ -143,7 +143,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { for (int i = 0; i < this.ComponentCount; i++) { - JpegComponent component = this.Components[i]; + IJpegComponent component = this.Components[i]; component.AllocateSpectral(fullScan); } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs index 40411ef28..532892e06 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs @@ -95,7 +95,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder } } - return this.pixelBuffer; + var buffer = this.pixelBuffer; + this.pixelBuffer = null; + return buffer; } /// @@ -111,7 +113,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder this.pixelRowsPerStep = majorVerticalSamplingFactor * blockPixelHeight; // pixel buffer for resulting image - this.pixelBuffer = allocator.Allocate2D(frame.PixelWidth, frame.PixelHeight); + this.pixelBuffer = allocator.Allocate2D( + frame.PixelWidth, + frame.PixelHeight, + this.configuration.PreferContiguousImageBuffers); this.paddedProxyPixelRow = allocator.Allocate(frame.PixelWidth + 3); // component processors from spectral to Rgba32 @@ -207,6 +212,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder this.rgbBuffer?.Dispose(); this.paddedProxyPixelRow?.Dispose(); + this.pixelBuffer?.Dispose(); } } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/LuminanceForwardConverter{TPixel}.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/LuminanceForwardConverter{TPixel}.cs index 6c402fcfd..e87f2fc57 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/LuminanceForwardConverter{TPixel}.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/LuminanceForwardConverter{TPixel}.cs @@ -2,8 +2,13 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +#if SUPPORTS_RUNTIME_INTRINSICS +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +#endif using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; @@ -74,6 +79,44 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder ref Block8x8F yBlock = ref this.Y; ref L8 l8Start = ref MemoryMarshal.GetReference(this.l8Span); + if (RgbToYCbCrConverterVectorized.IsSupported) + { + ConvertAvx(ref l8Start, ref yBlock); + } + else + { + ConvertScalar(ref l8Start, ref yBlock); + } + } + + /// + /// Converts 8x8 L8 pixel matrix to 8x8 Block of floats using Avx2 Intrinsics. + /// + /// Start of span of L8 pixels with size of 64 + /// 8x8 destination matrix of Luminance(Y) converted data + private static void ConvertAvx(ref L8 l8Start, ref Block8x8F yBlock) + { + Debug.Assert(RgbToYCbCrConverterVectorized.IsSupported, "AVX2 is required to run this converter"); + +#if SUPPORTS_RUNTIME_INTRINSICS + ref Vector128 l8ByteSpan = ref Unsafe.As>(ref l8Start); + ref Vector256 destRef = ref yBlock.V0; + + const int bytesPerL8Stride = 8; + for (nint i = 0; i < 8; i++) + { + Unsafe.Add(ref destRef, i) = Avx2.ConvertToVector256Single(Avx2.ConvertToVector256Int32(Unsafe.AddByteOffset(ref l8ByteSpan, bytesPerL8Stride * i))); + } +#endif + } + + /// + /// Converts 8x8 L8 pixel matrix to 8x8 Block of floats. + /// + /// Start of span of L8 pixels with size of 64 + /// 8x8 destination matrix of Luminance(Y) converted data + private static void ConvertScalar(ref L8 l8Start, ref Block8x8F yBlock) + { for (int i = 0; i < Block8x8F.Size; i++) { ref L8 c = ref Unsafe.Add(ref l8Start, i); diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/RgbForwardConverter{TPixel}.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/RgbForwardConverter{TPixel}.cs index 789277d7d..e2d12916c 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/RgbForwardConverter{TPixel}.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/RgbForwardConverter{TPixel}.cs @@ -2,8 +2,13 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +#if SUPPORTS_RUNTIME_INTRINSICS +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +#endif using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; @@ -94,10 +99,56 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder ref Block8x8F greenBlock = ref this.G; ref Block8x8F blueBlock = ref this.B; - CopyToBlock(this.rgbSpan, ref redBlock, ref greenBlock, ref blueBlock); + if (RgbToYCbCrConverterVectorized.IsSupported) + { + ConvertAvx(this.rgbSpan, ref redBlock, ref greenBlock, ref blueBlock); + } + else + { + ConvertScalar(this.rgbSpan, ref redBlock, ref greenBlock, ref blueBlock); + } + } + + /// + /// Converts 8x8 RGB24 pixel matrix to 8x8 Block of floats using Avx2 Intrinsics. + /// + /// Span of Rgb24 pixels with size of 64 + /// 8x8 destination matrix of Red converted data + /// 8x8 destination matrix of Blue converted data + /// 8x8 destination matrix of Green converted data + private static void ConvertAvx(Span rgbSpan, ref Block8x8F rBlock, ref Block8x8F gBlock, ref Block8x8F bBlock) + { + Debug.Assert(RgbToYCbCrConverterVectorized.IsSupported, "AVX2 is required to run this converter"); + +#if SUPPORTS_RUNTIME_INTRINSICS + ref Vector256 rgbByteSpan = ref Unsafe.As>(ref MemoryMarshal.GetReference(rgbSpan)); + ref Vector256 redRef = ref rBlock.V0; + ref Vector256 greenRef = ref gBlock.V0; + ref Vector256 blueRef = ref bBlock.V0; + var zero = Vector256.Create(0).AsByte(); + + var extractToLanesMask = Unsafe.As>(ref MemoryMarshal.GetReference(RgbToYCbCrConverterVectorized.MoveFirst24BytesToSeparateLanes)); + var extractRgbMask = Unsafe.As>(ref MemoryMarshal.GetReference(RgbToYCbCrConverterVectorized.ExtractRgb)); + Vector256 rgb, rg, bx; + + const int bytesPerRgbStride = 24; + for (nint i = 0; i < 8; i++) + { + rgb = Avx2.PermuteVar8x32(Unsafe.AddByteOffset(ref rgbByteSpan, bytesPerRgbStride * i).AsUInt32(), extractToLanesMask).AsByte(); + + rgb = Avx2.Shuffle(rgb, extractRgbMask); + + rg = Avx2.UnpackLow(rgb, zero); + bx = Avx2.UnpackHigh(rgb, zero); + + Unsafe.Add(ref redRef, i) = Avx.ConvertToVector256Single(Avx2.UnpackLow(rg, zero).AsInt32()); + Unsafe.Add(ref greenRef, i) = Avx.ConvertToVector256Single(Avx2.UnpackHigh(rg, zero).AsInt32()); + Unsafe.Add(ref blueRef, i) = Avx.ConvertToVector256Single(Avx2.UnpackLow(bx, zero).AsInt32()); + } +#endif } - private static void CopyToBlock(Span rgbSpan, ref Block8x8F redBlock, ref Block8x8F greenBlock, ref Block8x8F blueBlock) + private static void ConvertScalar(Span rgbSpan, ref Block8x8F redBlock, ref Block8x8F greenBlock, ref Block8x8F blueBlock) { ref Rgb24 rgbStart = ref MemoryMarshal.GetReference(rgbSpan); diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/RgbToYCbCrConverterVectorized.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/RgbToYCbCrConverterVectorized.cs index 9566ee862..d7542d7a5 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/RgbToYCbCrConverterVectorized.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/RgbToYCbCrConverterVectorized.cs @@ -60,13 +60,13 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder #if SUPPORTS_RUNTIME_INTRINSICS - private static ReadOnlySpan MoveFirst24BytesToSeparateLanes => new byte[] + internal static ReadOnlySpan MoveFirst24BytesToSeparateLanes => new byte[] { 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 6, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 7, 0, 0, 0 }; - private static ReadOnlySpan ExtractRgb => new byte[] + internal static ReadOnlySpan ExtractRgb => new byte[] { 0, 3, 6, 9, 1, 4, 7, 10, 2, 5, 8, 11, 0xFF, 0xFF, 0xFF, 0xFF, 0, 3, 6, 9, 1, 4, 7, 10, 2, 5, 8, 11, 0xFF, 0xFF, 0xFF, 0xFF diff --git a/src/ImageSharp/Formats/Jpeg/JpegConstants.cs b/src/ImageSharp/Formats/Jpeg/JpegConstants.cs index 89c4de550..20edf4037 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegConstants.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegConstants.cs @@ -306,17 +306,17 @@ namespace SixLabors.ImageSharp.Formats.Jpeg public const int RegisterSize = 64; /// - /// The number of bits to fetch when filling the buffer. + /// The number of bits to fetch when filling the buffer. /// public const int FetchBits = 48; /// - /// The number of times to read the input stream when filling the buffer. + /// The number of times to read the input stream when filling the buffer. /// public const int FetchLoop = FetchBits / 8; /// - /// The minimum number of bits allowed before by the before fetching. + /// The minimum number of bits allowed before by the before fetching. /// public const int MinBits = RegisterSize - FetchBits; diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs index 18212ffc7..22a9801b8 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs @@ -3,7 +3,6 @@ using System.IO; using System.Threading; -using System.Threading.Tasks; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Jpeg @@ -17,56 +16,26 @@ namespace SixLabors.ImageSharp.Formats.Jpeg public bool IgnoreMetadata { get; set; } /// - public Image Decode(Configuration configuration, Stream stream) + public Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { Guard.NotNull(stream, nameof(stream)); using var decoder = new JpegDecoderCore(configuration, this); - return decoder.Decode(configuration, stream); + return decoder.Decode(configuration, stream, cancellationToken); } /// - public Image Decode(Configuration configuration, Stream stream) - => this.Decode(configuration, stream); + public Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken) + => this.Decode(configuration, stream, cancellationToken); /// - public Task> DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel - { - Guard.NotNull(stream, nameof(stream)); - - using var decoder = new JpegDecoderCore(configuration, this); - return decoder.DecodeAsync(configuration, stream, cancellationToken); - } - - /// - public async Task DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) - => await this.DecodeAsync(configuration, stream, cancellationToken) - .ConfigureAwait(false); - - /// - public IImageInfo Identify(Configuration configuration, Stream stream) + public IImageInfo Identify(Configuration configuration, Stream stream, CancellationToken cancellationToken) { Guard.NotNull(stream, nameof(stream)); using var decoder = new JpegDecoderCore(configuration, this); - return decoder.Identify(configuration, stream); - } - - /// - public async Task IdentifyAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) - { - Guard.NotNull(stream, nameof(stream)); - - // The introduction of a local variable that refers to an object the implements - // IDisposable means you must use async/await, where the compiler generates the - // state machine and a continuation. - using (var decoder = new JpegDecoderCore(configuration, this)) - { - return await decoder.IdentifyAsync(configuration, stream, cancellationToken) - .ConfigureAwait(false); - } + return decoder.Identify(configuration, stream, cancellationToken); } } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 023928f37..a07db1c95 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -4,6 +4,7 @@ using System; using System.Buffers; using System.Buffers.Binary; +using System.Collections.Generic; using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -97,7 +98,17 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// Scan decoder. /// - private HuffmanScanDecoder scanDecoder; + private IJpegScanDecoder scanDecoder; + + /// + /// The arithmetic decoding tables. + /// + private List arithmeticDecodingTables; + + /// + /// The restart interval. + /// + private int? resetInterval; /// /// Initializes a new instance of the class. @@ -140,7 +151,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg public JpegComponent[] Components => this.Frame.Components; /// - IJpegComponent[] IRawJpegData.Components => this.Components; + JpegComponent[] IRawJpegData.Components => this.Components; /// public Block8x8F[] QuantizationTables { get; private set; } @@ -188,9 +199,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg { using var spectralConverter = new SpectralConverter(this.Configuration); - var scanDecoder = new HuffmanScanDecoder(stream, spectralConverter, cancellationToken); - - this.ParseStream(stream, scanDecoder, cancellationToken); + this.ParseStream(stream, spectralConverter, cancellationToken); this.InitExifProfile(); this.InitIccProfile(); this.InitIptcProfile(); @@ -206,7 +215,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { - this.ParseStream(stream, scanDecoder: null, cancellationToken); + this.ParseStream(stream, spectralConverter: null, cancellationToken); this.InitExifProfile(); this.InitIccProfile(); this.InitIptcProfile(); @@ -222,17 +231,22 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// so those tables do not need to be duplicated with segmented tiff's (tiff's with multiple strips). /// /// The table bytes. - /// The scan decoder. - public void LoadTables(byte[] tableBytes, HuffmanScanDecoder huffmanScanDecoder) + /// The scan decoder. + public void LoadTables(byte[] tableBytes, IJpegScanDecoder scanDecoder) { this.Metadata = new ImageMetadata(); this.QuantizationTables = new Block8x8F[4]; - this.scanDecoder = huffmanScanDecoder; + this.scanDecoder = scanDecoder; + if (tableBytes.Length < 4) + { + JpegThrowHelper.ThrowInvalidImageContentException("Not enough data to read marker"); + } + using var ms = new MemoryStream(tableBytes); using var stream = new BufferedReadStream(this.Configuration, ms); // Check for the Start Of Image marker. - stream.Read(this.markerBuffer, 0, 2); + int bytesRead = stream.Read(this.markerBuffer, 0, 2); var fileMarker = new JpegFileMarker(this.markerBuffer[1], 0); if (fileMarker.Marker != JpegConstants.Markers.SOI) { @@ -240,16 +254,23 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } // Read next marker. - stream.Read(this.markerBuffer, 0, 2); - byte marker = this.markerBuffer[1]; - fileMarker = new JpegFileMarker(marker, (int)stream.Position - 2); + bytesRead = stream.Read(this.markerBuffer, 0, 2); + fileMarker = new JpegFileMarker(this.markerBuffer[1], (int)stream.Position - 2); while (fileMarker.Marker != JpegConstants.Markers.EOI || (fileMarker.Marker == JpegConstants.Markers.EOI && fileMarker.Invalid)) { if (!fileMarker.Invalid) { // Get the marker length. - int remaining = this.ReadUint16(stream) - 2; + int markerContentByteSize = this.ReadUint16(stream) - 2; + + // Check whether stream actually has enought bytes to read + // markerContentByteSize is always positive so we cast + // to uint to avoid sign extension + if (stream.RemainingBytes < (uint)markerContentByteSize) + { + JpegThrowHelper.ThrowNotEnoughBytesForMarker(fileMarker.Marker); + } switch (fileMarker.Marker) { @@ -259,13 +280,13 @@ namespace SixLabors.ImageSharp.Formats.Jpeg case JpegConstants.Markers.RST7: break; case JpegConstants.Markers.DHT: - this.ProcessDefineHuffmanTablesMarker(stream, remaining); + this.ProcessDefineHuffmanTablesMarker(stream, markerContentByteSize); break; case JpegConstants.Markers.DQT: - this.ProcessDefineQuantizationTablesMarker(stream, remaining); + this.ProcessDefineQuantizationTablesMarker(stream, markerContentByteSize); break; case JpegConstants.Markers.DRI: - this.ProcessDefineRestartIntervalMarker(stream, remaining); + this.ProcessDefineRestartIntervalMarker(stream, markerContentByteSize); break; case JpegConstants.Markers.EOI: return; @@ -273,7 +294,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } // Read next marker. - stream.Read(this.markerBuffer, 0, 2); + bytesRead = stream.Read(this.markerBuffer, 0, 2); + if (bytesRead != 2) + { + JpegThrowHelper.ThrowInvalidImageContentException("Not enough data to read marker"); + } + fileMarker = new JpegFileMarker(this.markerBuffer[1], 0); } } @@ -282,13 +308,13 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// Parses the input stream for file markers. /// /// The input stream. - /// Scan decoder used exclusively to decode SOS marker. + /// The spectral converter to use. /// The token to monitor cancellation. - internal void ParseStream(BufferedReadStream stream, HuffmanScanDecoder scanDecoder, CancellationToken cancellationToken) + internal void ParseStream(BufferedReadStream stream, SpectralConverter spectralConverter, CancellationToken cancellationToken) { - bool metadataOnly = scanDecoder == null; + bool metadataOnly = spectralConverter == null; - this.scanDecoder = scanDecoder; + this.scanDecoder ??= new HuffmanScanDecoder(stream, spectralConverter, cancellationToken); this.Metadata = new ImageMetadata(); @@ -315,14 +341,36 @@ namespace SixLabors.ImageSharp.Formats.Jpeg if (!fileMarker.Invalid) { // Get the marker length. - int remaining = this.ReadUint16(stream) - 2; + int markerContentByteSize = this.ReadUint16(stream) - 2; + + // Check whether stream actually has enough bytes to read + // markerContentByteSize is always positive so we cast + // to uint to avoid sign extension. + if (stream.RemainingBytes < (uint)markerContentByteSize) + { + JpegThrowHelper.ThrowNotEnoughBytesForMarker(fileMarker.Marker); + } switch (fileMarker.Marker) { case JpegConstants.Markers.SOF0: case JpegConstants.Markers.SOF1: case JpegConstants.Markers.SOF2: - this.ProcessStartOfFrameMarker(stream, remaining, fileMarker, metadataOnly); + + this.ProcessStartOfFrameMarker(stream, markerContentByteSize, fileMarker, ComponentType.Huffman, metadataOnly); + break; + + case JpegConstants.Markers.SOF9: + case JpegConstants.Markers.SOF10: + case JpegConstants.Markers.SOF13: + case JpegConstants.Markers.SOF14: + this.scanDecoder = new ArithmeticScanDecoder(stream, spectralConverter, cancellationToken); + if (this.resetInterval.HasValue) + { + this.scanDecoder.ResetInterval = this.resetInterval.Value; + } + + this.ProcessStartOfFrameMarker(stream, markerContentByteSize, fileMarker, ComponentType.Arithmetic, metadataOnly); break; case JpegConstants.Markers.SOF5: @@ -338,19 +386,15 @@ namespace SixLabors.ImageSharp.Formats.Jpeg JpegThrowHelper.ThrowNotSupportedException("Decoding lossless jpeg files is not supported."); break; - case JpegConstants.Markers.SOF9: - case JpegConstants.Markers.SOF10: case JpegConstants.Markers.SOF11: - case JpegConstants.Markers.SOF13: - case JpegConstants.Markers.SOF14: case JpegConstants.Markers.SOF15: - JpegThrowHelper.ThrowNotSupportedException("Decoding jpeg files with arithmetic coding is not supported."); + JpegThrowHelper.ThrowNotSupportedException("Decoding jpeg files with lossless arithmetic coding is not supported."); break; case JpegConstants.Markers.SOS: if (!metadataOnly) { - this.ProcessStartOfScanMarker(stream, remaining); + this.ProcessStartOfScanMarker(stream, markerContentByteSize); break; } else @@ -364,41 +408,41 @@ namespace SixLabors.ImageSharp.Formats.Jpeg if (metadataOnly) { - stream.Skip(remaining); + stream.Skip(markerContentByteSize); } else { - this.ProcessDefineHuffmanTablesMarker(stream, remaining); + this.ProcessDefineHuffmanTablesMarker(stream, markerContentByteSize); } break; case JpegConstants.Markers.DQT: - this.ProcessDefineQuantizationTablesMarker(stream, remaining); + this.ProcessDefineQuantizationTablesMarker(stream, markerContentByteSize); break; case JpegConstants.Markers.DRI: if (metadataOnly) { - stream.Skip(remaining); + stream.Skip(markerContentByteSize); } else { - this.ProcessDefineRestartIntervalMarker(stream, remaining); + this.ProcessDefineRestartIntervalMarker(stream, markerContentByteSize); } break; case JpegConstants.Markers.APP0: - this.ProcessApplicationHeaderMarker(stream, remaining); + this.ProcessApplicationHeaderMarker(stream, markerContentByteSize); break; case JpegConstants.Markers.APP1: - this.ProcessApp1Marker(stream, remaining); + this.ProcessApp1Marker(stream, markerContentByteSize); break; case JpegConstants.Markers.APP2: - this.ProcessApp2Marker(stream, remaining); + this.ProcessApp2Marker(stream, markerContentByteSize); break; case JpegConstants.Markers.APP3: @@ -411,24 +455,32 @@ namespace SixLabors.ImageSharp.Formats.Jpeg case JpegConstants.Markers.APP10: case JpegConstants.Markers.APP11: case JpegConstants.Markers.APP12: - stream.Skip(remaining); + stream.Skip(markerContentByteSize); break; case JpegConstants.Markers.APP13: - this.ProcessApp13Marker(stream, remaining); + this.ProcessApp13Marker(stream, markerContentByteSize); break; case JpegConstants.Markers.APP14: - this.ProcessApp14Marker(stream, remaining); + this.ProcessApp14Marker(stream, markerContentByteSize); break; case JpegConstants.Markers.APP15: case JpegConstants.Markers.COM: - stream.Skip(remaining); + stream.Skip(markerContentByteSize); break; case JpegConstants.Markers.DAC: - JpegThrowHelper.ThrowNotSupportedException("Decoding jpeg files with arithmetic coding is not supported."); + if (metadataOnly) + { + stream.Skip(markerContentByteSize); + } + else + { + this.ProcessArithmeticTable(stream, markerContentByteSize); + } + break; } } @@ -677,7 +729,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } /// - /// Processes the App1 marker retrieving any stored metadata + /// Processes the App1 marker retrieving any stored metadata. /// /// The input stream. /// The remaining bytes in the segment block. @@ -687,7 +739,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg const int XmpMarkerLength = 29; if (remaining < ExifMarkerLength || this.IgnoreMetadata) { - // Skip the application header length + // Skip the application header length. stream.Skip(remaining); return; } @@ -697,12 +749,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg JpegThrowHelper.ThrowInvalidImageContentException("Bad App1 Marker length."); } - // XMP marker is the longest, so read at least that many bytes into temp. + // XMP marker is the longer then the EXIF marker, so first try read the EXIF marker bytes. stream.Read(this.temp, 0, ExifMarkerLength); + remaining -= ExifMarkerLength; if (ProfileResolver.IsProfile(this.temp, ProfileResolver.ExifMarker)) { - remaining -= ExifMarkerLength; this.hasExif = true; byte[] profile = new byte[remaining]; stream.Read(profile, 0, remaining); @@ -713,7 +765,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } else { - // If the EXIF information exceeds 64K, it will be split over multiple APP1 markers + // If the EXIF information exceeds 64K, it will be split over multiple APP1 markers. this.ExtendProfile(ref this.exifData, profile); } @@ -722,9 +774,17 @@ namespace SixLabors.ImageSharp.Formats.Jpeg if (ProfileResolver.IsProfile(this.temp, ProfileResolver.XmpMarker.Slice(0, ExifMarkerLength))) { - stream.Read(this.temp, 0, XmpMarkerLength - ExifMarkerLength); - remaining -= XmpMarkerLength; - if (ProfileResolver.IsProfile(this.temp, ProfileResolver.XmpMarker.Slice(ExifMarkerLength))) + const int remainingXmpMarkerBytes = XmpMarkerLength - ExifMarkerLength; + if (remaining < remainingXmpMarkerBytes || this.IgnoreMetadata) + { + // Skip the application header length. + stream.Skip(remaining); + return; + } + + stream.Read(this.temp, ExifMarkerLength, remainingXmpMarkerBytes); + remaining -= remainingXmpMarkerBytes; + if (ProfileResolver.IsProfile(this.temp, ProfileResolver.XmpMarker)) { this.hasXmp = true; byte[] profile = new byte[remaining]; @@ -736,7 +796,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } else { - // If the XMP information exceeds 64K, it will be split over multiple APP1 markers + // If the XMP information exceeds 64K, it will be split over multiple APP1 markers. this.ExtendProfile(ref this.xmpData, profile); } @@ -855,6 +915,47 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } } + /// + /// Processes a DAC marker, decoding the arithmetic tables. + /// + /// The input stream. + /// The remaining bytes in the segment block. + private void ProcessArithmeticTable(BufferedReadStream stream, int remaining) + { + this.arithmeticDecodingTables ??= new List(4); + + while (remaining > 0) + { + int tableClassAndIdentifier = stream.ReadByte(); + remaining--; + byte tableClass = (byte)(tableClassAndIdentifier >> 4); + byte identifier = (byte)(tableClassAndIdentifier & 0xF); + + byte conditioningTableValue = (byte)stream.ReadByte(); + remaining--; + + var arithmeticTable = new ArithmeticDecodingTable(tableClass, identifier); + arithmeticTable.Configure(conditioningTableValue); + + bool tableEntryReplaced = false; + for (int i = 0; i < this.arithmeticDecodingTables.Count; i++) + { + ArithmeticDecodingTable item = this.arithmeticDecodingTables[i]; + if (item.TableClass == arithmeticTable.TableClass && item.Identifier == arithmeticTable.Identifier) + { + this.arithmeticDecodingTables[i] = arithmeticTable; + tableEntryReplaced = true; + break; + } + } + + if (!tableEntryReplaced) + { + this.arithmeticDecodingTables.Add(arithmeticTable); + } + } + } + /// /// Reads the adobe image resource block name: a Pascal string (padded to make size even). /// @@ -916,7 +1017,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// The input stream. /// The remaining bytes in the segment block. /// - /// Thrown if the tables do not match the header + /// Thrown if the tables do not match the header. /// private void ProcessDefineQuantizationTablesMarker(BufferedReadStream stream, int remaining) { @@ -1022,8 +1123,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// The input stream. /// The remaining bytes in the segment block. /// The current frame marker. - /// Whether to parse metadata only - private void ProcessStartOfFrameMarker(BufferedReadStream stream, int remaining, in JpegFileMarker frameMarker, bool metadataOnly) + /// The jpeg decoding component type. + /// Whether to parse metadata only. + private void ProcessStartOfFrameMarker(BufferedReadStream stream, int remaining, in JpegFileMarker frameMarker, ComponentType decodingComponentType, bool metadataOnly) { if (this.Frame != null) { @@ -1035,17 +1137,21 @@ namespace SixLabors.ImageSharp.Formats.Jpeg JpegThrowHelper.ThrowInvalidImageContentException("Multiple SOF markers. Only single frame jpegs supported."); } - // Read initial marker definitions + // Read initial marker definitions. const int length = 6; - stream.Read(this.temp, 0, length); + int bytesRead = stream.Read(this.temp, 0, length); + if (bytesRead != length) + { + JpegThrowHelper.ThrowInvalidImageContentException("SOF marker does not contain enough data."); + } - // 1 byte: Bits/sample precision + // 1 byte: Bits/sample precision. byte precision = this.temp[0]; - // Validate: only 8-bit and 12-bit precisions are supported + // Validate: only 8-bit and 12-bit precisions are supported. if (Array.IndexOf(this.supportedPrecisions, precision) == -1) { - JpegThrowHelper.ThrowInvalidImageContentException("Only 8-Bit and 12-Bit precision supported."); + JpegThrowHelper.ThrowInvalidImageContentException("Only 8-Bit and 12-Bit precision is supported."); } // 2 byte: Height @@ -1054,18 +1160,18 @@ namespace SixLabors.ImageSharp.Formats.Jpeg // 2 byte: Width int frameWidth = (this.temp[3] << 8) | this.temp[4]; - // Validate: width/height > 0 (they are upper-bounded by 2 byte max value so no need to check that) + // Validate: width/height > 0 (they are upper-bounded by 2 byte max value so no need to check that). if (frameHeight == 0 || frameWidth == 0) { JpegThrowHelper.ThrowInvalidImageDimensions(frameWidth, frameHeight); } - // 1 byte: Number of components + // 1 byte: Number of components. byte componentCount = this.temp[5]; // Validate: componentCount more than 4 can lead to a buffer overflow during stream - // reading so we must limit it to 4 - // We do not support jpeg images with more than 4 components anyway + // reading so we must limit it to 4. + // We do not support jpeg images with more than 4 components anyway. if (componentCount > 4) { JpegThrowHelper.ThrowNotSupportedComponentCount(componentCount); @@ -1134,9 +1240,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg JpegThrowHelper.ThrowBadQuantizationTableIndex(quantTableIndex); } - var component = new JpegComponent(this.Configuration.MemoryAllocator, this.Frame, componentId, h, v, quantTableIndex, i); + IJpegComponent component = decodingComponentType is ComponentType.Huffman ? + new JpegComponent(this.Configuration.MemoryAllocator, this.Frame, componentId, h, v, quantTableIndex, i) : + new ArithmeticDecodingComponent(this.Configuration.MemoryAllocator, this.Frame, componentId, h, v, quantTableIndex, i); - this.Frame.Components[i] = component; + this.Frame.Components[i] = (JpegComponent)component; this.Frame.ComponentIds[i] = componentId; index += componentBytes; @@ -1164,11 +1272,17 @@ namespace SixLabors.ImageSharp.Formats.Jpeg const int codeValuesMaxByteSize = 256; const int totalBufferSize = codeLengthsByteSize + codeValuesMaxByteSize + HuffmanTable.WorkspaceByteSize; + var huffmanScanDecoder = this.scanDecoder as HuffmanScanDecoder; + if (huffmanScanDecoder is null) + { + JpegThrowHelper.ThrowInvalidImageContentException("missing huffman table data"); + } + int length = remaining; using (IMemoryOwner buffer = this.Configuration.MemoryAllocator.Allocate(totalBufferSize)) { Span bufferSpan = buffer.GetSpan(); - Span huffmanLegthsSpan = bufferSpan.Slice(0, codeLengthsByteSize); + Span huffmanLengthsSpan = bufferSpan.Slice(0, codeLengthsByteSize); Span huffmanValuesSpan = bufferSpan.Slice(codeLengthsByteSize, codeValuesMaxByteSize); Span tableWorkspace = MemoryMarshal.Cast(bufferSpan.Slice(codeLengthsByteSize + codeValuesMaxByteSize)); @@ -1190,12 +1304,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg JpegThrowHelper.ThrowInvalidImageContentException($"Bad huffman table index: {tableIndex}."); } - stream.Read(huffmanLegthsSpan, 1, 16); + stream.Read(huffmanLengthsSpan, 1, 16); int codeLengthSum = 0; for (int j = 1; j < 17; j++) { - codeLengthSum += huffmanLegthsSpan[j]; + codeLengthSum += huffmanLengthsSpan[j]; } length -= 17; @@ -1209,10 +1323,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg i += 17 + codeLengthSum; - this.scanDecoder.BuildHuffmanTable( + huffmanScanDecoder!.BuildHuffmanTable( tableType, tableIndex, - huffmanLegthsSpan, + huffmanLengthsSpan, huffmanValuesSpan.Slice(0, codeLengthSum), tableWorkspace); } @@ -1220,8 +1334,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } /// - /// Processes the DRI (Define Restart Interval Marker) Which specifies the interval between RSTn markers, in - /// macroblocks + /// Processes the DRI (Define Restart Interval Marker) Which specifies the interval between RSTn markers, + /// in macroblocks. /// /// The input stream. /// The remaining bytes in the segment block. @@ -1232,7 +1346,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg JpegThrowHelper.ThrowBadMarker(nameof(JpegConstants.Markers.DRI), remaining); } - this.scanDecoder.ResetInterval = this.ReadUint16(stream); + // Save the reset interval, because it can come before or after the SOF marker. + // If the reset interval comes after the SOF marker, the scanDecoder has not been created. + this.resetInterval = this.ReadUint16(stream); + + if (this.scanDecoder != null) + { + this.scanDecoder.ResetInterval = this.resetInterval.Value; + } } /// @@ -1245,7 +1366,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg JpegThrowHelper.ThrowInvalidImageContentException("No readable SOFn (Start Of Frame) marker found."); } - // 1 byte: Number of components in scan + // 1 byte: Number of components in scan. int selectorsCount = stream.ReadByte(); // Validate: 0 < count <= totalComponents @@ -1255,11 +1376,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg JpegThrowHelper.ThrowInvalidImageContentException($"Invalid number of components in scan: {selectorsCount}."); } - // Validate: marker must contain exactly (4 + selectorsCount*2) bytes + // Validate: Marker must contain exactly (4 + selectorsCount*2) bytes int selectorsBytes = selectorsCount * 2; if (remaining != 4 + selectorsBytes) { - JpegThrowHelper.ThrowBadMarker("SOS", remaining); + JpegThrowHelper.ThrowBadMarker(nameof(JpegConstants.Markers.SOS), remaining); } // selectorsCount*2 bytes: component index + huffman tables indices @@ -1282,7 +1403,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } } - // Validate: must be found among registered components + // Validate: Must be found among registered components. if (componentIndex == -1) { // TODO: extract as separate method? @@ -1291,7 +1412,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg this.Frame.ComponentOrder[i / 2] = (byte)componentIndex; - JpegComponent component = this.Frame.Components[componentIndex]; + IJpegComponent component = this.Frame.Components[componentIndex]; // 1 byte: Huffman table selectors. // 4 bits - dc @@ -1307,12 +1428,16 @@ namespace SixLabors.ImageSharp.Formats.Jpeg JpegThrowHelper.ThrowInvalidImageContentException($"Invalid huffman table for component:{componentSelectorId}: dc={dcTableIndex}, ac={acTableIndex}"); } - component.DCHuffmanTableId = dcTableIndex; - component.ACHuffmanTableId = acTableIndex; + component.DcTableId = dcTableIndex; + component.AcTableId = acTableIndex; } - // 3 bytes: Progressive scan decoding data - stream.Read(this.temp, 0, 3); + // 3 bytes: Progressive scan decoding data. + int bytesRead = stream.Read(this.temp, 0, 3); + if (bytesRead != 3) + { + JpegThrowHelper.ThrowInvalidImageContentException("Not enough data to read progressive scan decoding data"); + } int spectralStart = this.temp[0]; this.scanDecoder.SpectralStart = spectralStart; @@ -1324,18 +1449,28 @@ namespace SixLabors.ImageSharp.Formats.Jpeg this.scanDecoder.SuccessiveHigh = successiveApproximation >> 4; this.scanDecoder.SuccessiveLow = successiveApproximation & 15; + if (this.scanDecoder is ArithmeticScanDecoder arithmeticScanDecoder) + { + arithmeticScanDecoder.InitDecodingTables(this.arithmeticDecodingTables); + } + this.scanDecoder.ParseEntropyCodedData(selectorsCount); } /// - /// Reads a from the stream advancing it by two bytes + /// Reads a from the stream advancing it by two bytes. /// /// The input stream. /// The [MethodImpl(InliningOptions.ShortMethod)] private ushort ReadUint16(BufferedReadStream stream) { - stream.Read(this.markerBuffer, 0, 2); + int bytesRead = stream.Read(this.markerBuffer, 0, 2); + if (bytesRead != 2) + { + JpegThrowHelper.ThrowInvalidImageContentException("jpeg stream does not contain enough data, could not read ushort."); + } + return BinaryPrimitives.ReadUInt16BigEndian(this.markerBuffer); } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegThrowHelper.cs b/src/ImageSharp/Formats/Jpeg/JpegThrowHelper.cs index b238e45ef..1073ffff7 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegThrowHelper.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegThrowHelper.cs @@ -25,6 +25,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg [MethodImpl(InliningOptions.ColdPath)] public static void ThrowBadMarker(string marker, int length) => throw new InvalidImageContentException($"Marker {marker} has bad length {length}."); + [MethodImpl(InliningOptions.ColdPath)] + public static void ThrowNotEnoughBytesForMarker(byte marker) => throw new InvalidImageContentException($"Input stream does not have enough bytes to parse declared contents of the {marker:X2} marker."); + [MethodImpl(InliningOptions.ColdPath)] public static void ThrowBadQuantizationTableIndex(int index) => throw new InvalidImageContentException($"Bad Quantization Table index {index}."); diff --git a/src/ImageSharp/Formats/Pbm/PbmDecoder.cs b/src/ImageSharp/Formats/Pbm/PbmDecoder.cs index 2eebbb1d9..97a9cb7d7 100644 --- a/src/ImageSharp/Formats/Pbm/PbmDecoder.cs +++ b/src/ImageSharp/Formats/Pbm/PbmDecoder.cs @@ -3,7 +3,6 @@ using System.IO; using System.Threading; -using System.Threading.Tasks; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Pbm @@ -30,50 +29,26 @@ namespace SixLabors.ImageSharp.Formats.Pbm public sealed class PbmDecoder : IImageDecoder, IImageInfoDetector { /// - public Image Decode(Configuration configuration, Stream stream) + public Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { Guard.NotNull(stream, nameof(stream)); var decoder = new PbmDecoderCore(configuration); - return decoder.Decode(configuration, stream); + return decoder.Decode(configuration, stream, cancellationToken); } /// - public Image Decode(Configuration configuration, Stream stream) - => this.Decode(configuration, stream); + public Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken) + => this.Decode(configuration, stream, cancellationToken); /// - public Task> DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel - { - Guard.NotNull(stream, nameof(stream)); - - var decoder = new PbmDecoderCore(configuration); - return decoder.DecodeAsync(configuration, stream, cancellationToken); - } - - /// - public async Task DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) - => await this.DecodeAsync(configuration, stream, cancellationToken) - .ConfigureAwait(false); - - /// - public IImageInfo Identify(Configuration configuration, Stream stream) - { - Guard.NotNull(stream, nameof(stream)); - - var decoder = new PbmDecoderCore(configuration); - return decoder.Identify(configuration, stream); - } - - /// - public Task IdentifyAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) + public IImageInfo Identify(Configuration configuration, Stream stream, CancellationToken cancellationToken) { Guard.NotNull(stream, nameof(stream)); var decoder = new PbmDecoderCore(configuration); - return decoder.IdentifyAsync(configuration, stream, cancellationToken); + return decoder.Identify(configuration, stream, cancellationToken); } } } diff --git a/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs b/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs index 83c638934..44a16f154 100644 --- a/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs @@ -20,9 +20,9 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters internal static class AverageFilter { /// - /// Decodes the scanline + /// Decodes a scanline, which was filtered with the average filter. /// - /// The scanline to decode + /// The scanline to decode. /// The previous scanline. /// The bytes per pixel. [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -30,11 +30,66 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters { DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline)); + // The Avg filter predicts each pixel as the (truncated) average of a and b: + // Average(x) + floor((Raw(x-bpp)+Prior(x))/2) + // With pixels positioned like this: + // prev: c b + // row: a d +#if SUPPORTS_RUNTIME_INTRINSICS + if (Sse2.IsSupported && bytesPerPixel is 4) + { + DecodeSse2(scanline, previousScanline); + } + else +#endif + { + DecodeScalar(scanline, previousScanline, bytesPerPixel); + } + } + +#if SUPPORTS_RUNTIME_INTRINSICS + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void DecodeSse2(Span scanline, Span previousScanline) + { ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); - // Average(x) + floor((Raw(x-bpp)+Prior(x))/2) - int x = 1; + Vector128 d = Vector128.Zero; + var ones = Vector128.Create((byte)1); + + int rb = scanline.Length; + nint offset = 1; + while (rb >= 4) + { + ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset); + Vector128 a = d; + Vector128 b = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref Unsafe.Add(ref prevBaseRef, offset))).AsByte(); + d = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref scanRef)).AsByte(); + + // PNG requires a truncating average, so we can't just use _mm_avg_epu8, + // but we can fix it up by subtracting off 1 if it rounded up. + Vector128 avg = Sse2.Average(a, b); + Vector128 xor = Sse2.Xor(a, b); + Vector128 and = Sse2.And(xor, ones); + avg = Sse2.Subtract(avg, and); + d = Sse2.Add(d, avg); + + // Store the result. + Unsafe.As(ref scanRef) = Sse2.ConvertToInt32(d.AsInt32()); + + rb -= 4; + offset += 4; + } + } +#endif + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void DecodeScalar(Span scanline, Span previousScanline, int bytesPerPixel) + { + ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); + ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); + + nint x = 1; for (; x <= bytesPerPixel /* Note the <= because x starts at 1 */; ++x) { ref byte scan = ref Unsafe.Add(ref scanBaseRef, x); @@ -52,13 +107,13 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters } /// - /// Encodes the scanline + /// Encodes a scanline with the average filter applied. /// - /// The scanline to encode + /// The scanline to encode. /// The previous scanline. /// The filtered scanline result. /// The bytes per pixel. - /// The sum of the total variance of the filtered row + /// The sum of the total variance of the filtered row. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Encode(ReadOnlySpan scanline, ReadOnlySpan previousScanline, Span result, int bytesPerPixel, out int sum) { diff --git a/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs b/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs index 6a89a1122..0553eb46a 100644 --- a/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs @@ -22,9 +22,9 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters internal static class PaethFilter { /// - /// Decodes the scanline + /// Decodes a scanline, which was filtered with the paeth filter. /// - /// The scanline to decode + /// The scanline to decode. /// The previous scanline. /// The bytes per pixel. [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -32,6 +32,86 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters { DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline)); + // Paeth tries to predict pixel d using the pixel to the left of it, a, + // and two pixels from the previous row, b and c: + // prev: c b + // row: a d + // The Paeth function predicts d to be whichever of a, b, or c is nearest to + // p = a + b - c. +#if SUPPORTS_RUNTIME_INTRINSICS + if (Sse41.IsSupported && bytesPerPixel is 4) + { + DecodeSse41(scanline, previousScanline); + } + else +#endif + { + DecodeScalar(scanline, previousScanline, bytesPerPixel); + } + } + +#if SUPPORTS_RUNTIME_INTRINSICS + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void DecodeSse41(Span scanline, Span previousScanline) + { + ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); + ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); + + Vector128 b = Vector128.Zero; + Vector128 d = Vector128.Zero; + + int rb = scanline.Length; + nint offset = 1; + while (rb >= 4) + { + ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset); + + // It's easiest to do this math (particularly, deal with pc) with 16-bit intermediates. + Vector128 c = b; + Vector128 a = d; + b = Sse2.UnpackLow( + Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref Unsafe.Add(ref prevBaseRef, offset))).AsByte(), + Vector128.Zero); + d = Sse2.UnpackLow( + Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref scanRef)).AsByte(), + Vector128.Zero); + + // (p-a) == (a+b-c - a) == (b-c) + Vector128 pa = Sse2.Subtract(b.AsInt16(), c.AsInt16()); + + // (p-b) == (a+b-c - b) == (a-c) + Vector128 pb = Sse2.Subtract(a.AsInt16(), c.AsInt16()); + + // (p-c) == (a+b-c - c) == (a+b-c-c) == (b-c)+(a-c) + Vector128 pc = Sse2.Add(pa.AsInt16(), pb.AsInt16()); + + pa = Ssse3.Abs(pa.AsInt16()).AsInt16(); /* |p-a| */ + pb = Ssse3.Abs(pb.AsInt16()).AsInt16(); /* |p-b| */ + pc = Ssse3.Abs(pc.AsInt16()).AsInt16(); /* |p-c| */ + + Vector128 smallest = Sse2.Min(pc, Sse2.Min(pa, pb)); + + // Paeth breaks ties favoring a over b over c. + Vector128 mask = Sse41.BlendVariable(c, b, Sse2.CompareEqual(smallest, pb).AsByte()); + Vector128 nearest = Sse41.BlendVariable(mask, a, Sse2.CompareEqual(smallest, pa).AsByte()); + + // Note `_epi8`: we need addition to wrap modulo 255. + d = Sse2.Add(d, nearest); + + // Store the result. + Unsafe.As(ref scanRef) = Sse2.ConvertToInt32(Sse2.PackUnsignedSaturate(d.AsInt16(), d.AsInt16()).AsInt32()); + + rb -= 4; + offset += 4; + } + } + +#endif + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void DecodeScalar(Span scanline, Span previousScanline, int bytesPerPixel) + { ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); @@ -56,13 +136,13 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters } /// - /// Encodes the scanline + /// Encodes a scanline and applies the paeth filter. /// /// The scanline to encode /// The previous scanline. /// The filtered scanline result. /// The bytes per pixel. - /// The sum of the total variance of the filtered row + /// The sum of the total variance of the filtered row. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Encode(ReadOnlySpan scanline, ReadOnlySpan previousScanline, Span result, int bytesPerPixel, out int sum) { diff --git a/src/ImageSharp/Formats/Png/Filters/SubFilter.cs b/src/ImageSharp/Formats/Png/Filters/SubFilter.cs index c28b877e4..eaa4dc034 100644 --- a/src/ImageSharp/Formats/Png/Filters/SubFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/SubFilter.cs @@ -21,17 +21,57 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters internal static class SubFilter { /// - /// Decodes the scanline + /// Decodes a scanline, which was filtered with the sub filter. /// - /// The scanline to decode + /// The scanline to decode. /// The bytes per pixel. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Decode(Span scanline, int bytesPerPixel) + { + // The Sub filter predicts each pixel as the previous pixel. +#if SUPPORTS_RUNTIME_INTRINSICS + if (Sse2.IsSupported && bytesPerPixel is 4) + { + DecodeSse2(scanline); + } + else +#endif + { + DecodeScalar(scanline, bytesPerPixel); + } + } + +#if SUPPORTS_RUNTIME_INTRINSICS + private static void DecodeSse2(Span scanline) + { + ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); + + Vector128 d = Vector128.Zero; + + int rb = scanline.Length; + nint offset = 1; + while (rb >= 4) + { + ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset); + Vector128 a = d; + d = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref scanRef)).AsByte(); + + d = Sse2.Add(d, a); + + Unsafe.As(ref scanRef) = Sse2.ConvertToInt32(d.AsInt32()); + + rb -= 4; + offset += 4; + } + } +#endif + + private static void DecodeScalar(Span scanline, int bytesPerPixel) { ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); // Sub(x) + Raw(x-bpp) - int x = bytesPerPixel + 1; + nint x = bytesPerPixel + 1; Unsafe.Add(ref scanBaseRef, x); for (; x < scanline.Length; ++x) { @@ -42,12 +82,12 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters } /// - /// Encodes the scanline + /// Encodes a scanline with the sup filter applied. /// - /// The scanline to encode + /// The scanline to encode. /// The filtered scanline result. /// The bytes per pixel. - /// The sum of the total variance of the filtered row + /// The sum of the total variance of the filtered row. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Encode(ReadOnlySpan scanline, ReadOnlySpan result, int bytesPerPixel, out int sum) { diff --git a/src/ImageSharp/Formats/Png/Filters/UpFilter.cs b/src/ImageSharp/Formats/Png/Filters/UpFilter.cs index 7e0286991..0d24d9c5d 100644 --- a/src/ImageSharp/Formats/Png/Filters/UpFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/UpFilter.cs @@ -21,7 +21,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters internal static class UpFilter { /// - /// Decodes the scanline + /// Decodes a scanline, which was filtered with the up filter. /// /// The scanline to decode /// The previous scanline. @@ -30,6 +30,91 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters { DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline)); +#if SUPPORTS_RUNTIME_INTRINSICS + if (Avx2.IsSupported) + { + DecodeAvx2(scanline, previousScanline); + } + else if (Sse2.IsSupported) + { + DecodeSse2(scanline, previousScanline); + } + else +#endif + { + DecodeScalar(scanline, previousScanline); + } + } + +#if SUPPORTS_RUNTIME_INTRINSICS + private static void DecodeAvx2(Span scanline, Span previousScanline) + { + ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); + ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); + + // Up(x) + Prior(x) + int rb = scanline.Length; + nint offset = 1; + const int bytesPerBatch = 32; + while (rb >= bytesPerBatch) + { + ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset); + Vector256 current = Unsafe.As>(ref scanRef); + Vector256 up = Unsafe.As>(ref Unsafe.Add(ref prevBaseRef, offset)); + + Vector256 sum = Avx2.Add(up, current); + Unsafe.As>(ref scanRef) = sum; + + offset += bytesPerBatch; + rb -= bytesPerBatch; + } + + // Handle left over. + for (nint i = offset; i < scanline.Length; i++) + { + ref byte scan = ref Unsafe.Add(ref scanBaseRef, offset); + byte above = Unsafe.Add(ref prevBaseRef, offset); + scan = (byte)(scan + above); + offset++; + } + } + + private static void DecodeSse2(Span scanline, Span previousScanline) + { + ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); + ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); + + // Up(x) + Prior(x) + int rb = scanline.Length; + nint offset = 1; + const int bytesPerBatch = 16; + while (rb >= bytesPerBatch) + { + ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset); + Vector128 current = Unsafe.As>(ref scanRef); + Vector128 up = Unsafe.As>(ref Unsafe.Add(ref prevBaseRef, offset)); + + Vector128 sum = Sse2.Add(up, current); + Unsafe.As>(ref scanRef) = sum; + + offset += bytesPerBatch; + rb -= bytesPerBatch; + } + + // Handle left over. + for (nint i = offset; i < scanline.Length; i++) + { + ref byte scan = ref Unsafe.Add(ref scanBaseRef, offset); + byte above = Unsafe.Add(ref prevBaseRef, offset); + scan = (byte)(scan + above); + offset++; + } + } +#endif + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void DecodeScalar(Span scanline, Span previousScanline) + { ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); @@ -43,12 +128,12 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters } /// - /// Encodes the scanline + /// Encodes a scanline with the up filter applied. /// - /// The scanline to encode + /// The scanline to encode. /// The previous scanline. /// The filtered scanline result. - /// The sum of the total variance of the filtered row + /// The sum of the total variance of the filtered row. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Encode(ReadOnlySpan scanline, ReadOnlySpan previousScanline, Span result, out int sum) { diff --git a/src/ImageSharp/Formats/Png/PngDecoder.cs b/src/ImageSharp/Formats/Png/PngDecoder.cs index 04e70c51d..0b233848a 100644 --- a/src/ImageSharp/Formats/Png/PngDecoder.cs +++ b/src/ImageSharp/Formats/Png/PngDecoder.cs @@ -3,7 +3,6 @@ using System.IO; using System.Threading; -using System.Threading.Tasks; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Png @@ -17,18 +16,18 @@ namespace SixLabors.ImageSharp.Formats.Png public bool IgnoreMetadata { get; set; } /// - public Image Decode(Configuration configuration, Stream stream) + public Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { PngDecoderCore decoder = new(configuration, this); - return decoder.Decode(configuration, stream); + return decoder.Decode(configuration, stream, cancellationToken); } /// - public Image Decode(Configuration configuration, Stream stream) + public Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken) { PngDecoderCore decoder = new(configuration, true); - IImageInfo info = decoder.Identify(configuration, stream); + IImageInfo info = decoder.Identify(configuration, stream, cancellationToken); stream.Position = 0; PngMetadata meta = info.Metadata.GetPngMetadata(); @@ -40,118 +39,49 @@ namespace SixLabors.ImageSharp.Formats.Png if (bits == PngBitDepth.Bit16) { return !meta.HasTransparency - ? this.Decode(configuration, stream) - : this.Decode(configuration, stream); + ? this.Decode(configuration, stream, cancellationToken) + : this.Decode(configuration, stream, cancellationToken); } return !meta.HasTransparency - ? this.Decode(configuration, stream) - : this.Decode(configuration, stream); + ? this.Decode(configuration, stream, cancellationToken) + : this.Decode(configuration, stream, cancellationToken); case PngColorType.Rgb: if (bits == PngBitDepth.Bit16) { return !meta.HasTransparency - ? this.Decode(configuration, stream) - : this.Decode(configuration, stream); + ? this.Decode(configuration, stream, cancellationToken) + : this.Decode(configuration, stream, cancellationToken); } return !meta.HasTransparency - ? this.Decode(configuration, stream) - : this.Decode(configuration, stream); + ? this.Decode(configuration, stream, cancellationToken) + : this.Decode(configuration, stream, cancellationToken); case PngColorType.Palette: - return this.Decode(configuration, stream); + return this.Decode(configuration, stream, cancellationToken); case PngColorType.GrayscaleWithAlpha: return (bits == PngBitDepth.Bit16) - ? this.Decode(configuration, stream) - : this.Decode(configuration, stream); + ? this.Decode(configuration, stream, cancellationToken) + : this.Decode(configuration, stream, cancellationToken); case PngColorType.RgbWithAlpha: return (bits == PngBitDepth.Bit16) - ? this.Decode(configuration, stream) - : this.Decode(configuration, stream); + ? this.Decode(configuration, stream, cancellationToken) + : this.Decode(configuration, stream, cancellationToken); default: - return this.Decode(configuration, stream); + return this.Decode(configuration, stream, cancellationToken); } } /// - public Task> DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel - { - PngDecoderCore decoder = new(configuration, this); - return decoder.DecodeAsync(configuration, stream, cancellationToken); - } - - /// - public async Task DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) - { - PngDecoderCore decoder = new(configuration, true); - IImageInfo info = await decoder.IdentifyAsync(configuration, stream, cancellationToken).ConfigureAwait(false); - stream.Position = 0; - - PngMetadata meta = info.Metadata.GetPngMetadata(); - PngColorType color = meta.ColorType.GetValueOrDefault(); - PngBitDepth bits = meta.BitDepth.GetValueOrDefault(); - switch (color) - { - case PngColorType.Grayscale: - if (bits == PngBitDepth.Bit16) - { - return !meta.HasTransparency - ? await this.DecodeAsync(configuration, stream, cancellationToken).ConfigureAwait(false) - : await this.DecodeAsync(configuration, stream, cancellationToken).ConfigureAwait(false); - } - - return !meta.HasTransparency - ? await this.DecodeAsync(configuration, stream, cancellationToken).ConfigureAwait(false) - : await this.DecodeAsync(configuration, stream, cancellationToken).ConfigureAwait(false); - - case PngColorType.Rgb: - if (bits == PngBitDepth.Bit16) - { - return !meta.HasTransparency - ? await this.DecodeAsync(configuration, stream, cancellationToken).ConfigureAwait(false) - : await this.DecodeAsync(configuration, stream, cancellationToken).ConfigureAwait(false); - } - - return !meta.HasTransparency - ? await this.DecodeAsync(configuration, stream, cancellationToken).ConfigureAwait(false) - : await this.DecodeAsync(configuration, stream, cancellationToken).ConfigureAwait(false); - - case PngColorType.Palette: - return await this.DecodeAsync(configuration, stream, cancellationToken).ConfigureAwait(false); - - case PngColorType.GrayscaleWithAlpha: - return (bits == PngBitDepth.Bit16) - ? await this.DecodeAsync(configuration, stream, cancellationToken).ConfigureAwait(false) - : await this.DecodeAsync(configuration, stream, cancellationToken).ConfigureAwait(false); - - case PngColorType.RgbWithAlpha: - return (bits == PngBitDepth.Bit16) - ? await this.DecodeAsync(configuration, stream, cancellationToken).ConfigureAwait(false) - : await this.DecodeAsync(configuration, stream, cancellationToken).ConfigureAwait(false); - - default: - return await this.DecodeAsync(configuration, stream, cancellationToken).ConfigureAwait(false); - } - } - - /// - public IImageInfo Identify(Configuration configuration, Stream stream) - { - PngDecoderCore decoder = new(configuration, this); - return decoder.Identify(configuration, stream); - } - - /// - public Task IdentifyAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) + public IImageInfo Identify(Configuration configuration, Stream stream, CancellationToken cancellationToken) { PngDecoderCore decoder = new(configuration, this); - return decoder.IdentifyAsync(configuration, stream, cancellationToken); + return decoder.Identify(configuration, stream, cancellationToken); } } } diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index f5fc86ee4..f46b5058a 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -19,6 +19,7 @@ using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; @@ -205,6 +206,9 @@ namespace SixLabors.ImageSharp.Formats.Png this.MergeOrSetExifProfile(metadata, new ExifProfile(exifData), replaceExistingKeys: true); } + break; + case PngChunkType.EmbeddedColorProfile: + this.ReadColorProfileChunk(metadata, chunk.Data.GetSpan()); break; case PngChunkType.End: goto EOF; @@ -227,10 +231,16 @@ namespace SixLabors.ImageSharp.Formats.Png return image; } + catch + { + image?.Dispose(); + throw; + } finally { this.scanline?.Dispose(); this.previousScanline?.Dispose(); + this.nextChunk?.Data?.Dispose(); } } @@ -336,6 +346,14 @@ namespace SixLabors.ImageSharp.Formats.Png break; case PngChunkType.End: goto EOF; + + default: + if (this.colorMetadataOnly) + { + this.SkipChunkDataAndCrc(chunk); + } + + break; } } finally @@ -429,10 +447,17 @@ namespace SixLabors.ImageSharp.Formats.Png /// The metadata to read to. /// The data containing physical data. private void ReadGammaChunk(PngMetadata pngMetadata, ReadOnlySpan data) + { + if (data.Length < 4) + { + // Ignore invalid gamma chunks. + return; + } - // The value is encoded as a 4-byte unsigned integer, representing gamma times 100000. // For example, a gamma of 1/2.2 would be stored as 45455. - => pngMetadata.Gamma = BinaryPrimitives.ReadUInt32BigEndian(data) * 1e-5F; + // The value is encoded as a 4-byte unsigned integer, representing gamma times 100000. + pngMetadata.Gamma = BinaryPrimitives.ReadUInt32BigEndian(data) * 1e-5F; + } /// /// Initializes the image and various buffers needed for processing @@ -457,6 +482,8 @@ namespace SixLabors.ImageSharp.Formats.Png this.bytesPerSample = this.header.BitDepth / 8; } + this.previousScanline?.Dispose(); + this.scanline?.Dispose(); this.previousScanline = this.memoryAllocator.Allocate(this.bytesPerScanline, AllocationOptions.Clean); this.scanline = this.Configuration.MemoryAllocator.Allocate(this.bytesPerScanline, AllocationOptions.Clean); } @@ -1151,6 +1178,76 @@ namespace SixLabors.ImageSharp.Formats.Png return true; } + /// + /// Reads the color profile chunk. The data is stored similar to the zTXt chunk. + /// + /// The metadata. + /// The bytes containing the profile. + private void ReadColorProfileChunk(ImageMetadata metadata, ReadOnlySpan data) + { + int zeroIndex = data.IndexOf((byte)0); + if (zeroIndex is < PngConstants.MinTextKeywordLength or > PngConstants.MaxTextKeywordLength) + { + return; + } + + byte compressionMethod = data[zeroIndex + 1]; + if (compressionMethod != 0) + { + // Only compression method 0 is supported (zlib datastream with deflate compression). + return; + } + + ReadOnlySpan keywordBytes = data.Slice(0, zeroIndex); + if (!this.TryReadTextKeyword(keywordBytes, out string name)) + { + return; + } + + ReadOnlySpan compressedData = data.Slice(zeroIndex + 2); + + if (this.TryUncompressZlibData(compressedData, out byte[] iccpProfileBytes)) + { + metadata.IccProfile = new IccProfile(iccpProfileBytes); + } + } + + /// + /// Tries to un-compress zlib compressed data. + /// + /// The compressed data. + /// The uncompressed bytes array. + /// True, if de-compressing was successful. + private unsafe bool TryUncompressZlibData(ReadOnlySpan compressedData, out byte[] uncompressedBytesArray) + { + fixed (byte* compressedDataBase = compressedData) + { + using (IMemoryOwner destBuffer = this.memoryAllocator.Allocate(this.Configuration.StreamProcessingBufferSize)) + using (var memoryStreamOutput = new MemoryStream(compressedData.Length)) + using (var memoryStreamInput = new UnmanagedMemoryStream(compressedDataBase, compressedData.Length)) + using (var bufferedStream = new BufferedReadStream(this.Configuration, memoryStreamInput)) + using (var inflateStream = new ZlibInflateStream(bufferedStream)) + { + Span destUncompressedData = destBuffer.GetSpan(); + if (!inflateStream.AllocateNewBytes(compressedData.Length, false)) + { + uncompressedBytesArray = Array.Empty(); + return false; + } + + int bytesRead = inflateStream.CompressedStream.Read(destUncompressedData, 0, destUncompressedData.Length); + while (bytesRead != 0) + { + memoryStreamOutput.Write(destUncompressedData.Slice(0, bytesRead)); + bytesRead = inflateStream.CompressedStream.Read(destUncompressedData, 0, destUncompressedData.Length); + } + + uncompressedBytesArray = memoryStreamOutput.ToArray(); + return true; + } + } + } + /// /// Compares two ReadOnlySpan<char>s in a case-insensitive method. /// This is only needed because older frameworks are missing the extension method. @@ -1283,7 +1380,7 @@ namespace SixLabors.ImageSharp.Formats.Png } else if (this.IsXmpTextData(keywordBytes)) { - XmpProfile xmpProfile = new XmpProfile(data.Slice(dataStartIdx).ToArray()); + var xmpProfile = new XmpProfile(data.Slice(dataStartIdx).ToArray()); metadata.XmpProfile = xmpProfile; } else @@ -1302,29 +1399,14 @@ namespace SixLabors.ImageSharp.Formats.Png /// The . private bool TryUncompressTextData(ReadOnlySpan compressedData, Encoding encoding, out string value) { - using (var memoryStream = new MemoryStream(compressedData.ToArray())) - using (var bufferedStream = new BufferedReadStream(this.Configuration, memoryStream)) - using (var inflateStream = new ZlibInflateStream(bufferedStream)) + if (this.TryUncompressZlibData(compressedData, out byte[] uncompressedData)) { - if (!inflateStream.AllocateNewBytes(compressedData.Length, false)) - { - value = null; - return false; - } - - var uncompressedBytes = new List(); - - // Note: this uses a buffer which is only 4 bytes long to read the stream, maybe allocating a larger buffer makes sense here. - int bytesRead = inflateStream.CompressedStream.Read(this.buffer, 0, this.buffer.Length); - while (bytesRead != 0) - { - uncompressedBytes.AddRange(this.buffer.AsSpan(0, bytesRead).ToArray()); - bytesRead = inflateStream.CompressedStream.Read(this.buffer, 0, this.buffer.Length); - } - - value = encoding.GetString(uncompressedBytes.ToArray()); + value = encoding.GetString(uncompressedData); return true; } + + value = null; + return false; } /// @@ -1344,6 +1426,7 @@ namespace SixLabors.ImageSharp.Formats.Png { if (chunk.Type == PngChunkType.Data) { + chunk.Data?.Dispose(); return chunk.Length; } @@ -1438,6 +1521,9 @@ namespace SixLabors.ImageSharp.Formats.Png if (validCrc != inputCrc) { string chunkTypeName = Encoding.ASCII.GetString(chunkType); + + // ensure when throwing we dispose the data back to the memory allocator + chunk.Data?.Dispose(); PngThrowHelper.ThrowInvalidChunkCrc(chunkTypeName); } } diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index c443c0fcf..ad16c8037 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -87,6 +87,11 @@ namespace SixLabors.ImageSharp.Formats.Png /// private IMemoryOwner currentScanline; + /// + /// The color profile name. + /// + private const string ColorProfileName = "ICC Profile"; + /// /// Initializes a new instance of the class. /// @@ -134,6 +139,7 @@ namespace SixLabors.ImageSharp.Formats.Png this.WriteHeaderChunk(stream); this.WriteGammaChunk(stream); + this.WriteColorProfileChunk(stream, metadata); this.WritePaletteChunk(stream, quantized); this.WriteTransparencyChunk(stream, pngMetadata); this.WritePhysicalChunk(stream, metadata); @@ -656,7 +662,7 @@ namespace SixLabors.ImageSharp.Formats.Png } /// - /// Writes an iTXT chunk, containing the XMP metdata to the stream, if such profile is present in the metadata. + /// Writes an iTXT chunk, containing the XMP metadata to the stream, if such profile is present in the metadata. /// /// The containing image data. /// The image metadata. @@ -673,7 +679,7 @@ namespace SixLabors.ImageSharp.Formats.Png return; } - var xmpData = meta.XmpProfile.Data; + byte[] xmpData = meta.XmpProfile.Data; if (xmpData.Length == 0) { @@ -687,19 +693,49 @@ namespace SixLabors.ImageSharp.Formats.Png PngConstants.XmpKeyword.CopyTo(payload); int bytesWritten = PngConstants.XmpKeyword.Length; - // Write the iTxt header (all zeros in this case) - payload[bytesWritten++] = 0; - payload[bytesWritten++] = 0; - payload[bytesWritten++] = 0; - payload[bytesWritten++] = 0; - payload[bytesWritten++] = 0; + // Write the iTxt header (all zeros in this case). + Span iTxtHeader = payload.Slice(bytesWritten); + iTxtHeader[4] = 0; + iTxtHeader[3] = 0; + iTxtHeader[2] = 0; + iTxtHeader[1] = 0; + iTxtHeader[0] = 0; + bytesWritten += 5; - // And the XMP data itself + // And the XMP data itself. xmpData.CopyTo(payload.Slice(bytesWritten)); this.WriteChunk(stream, PngChunkType.InternationalText, payload); } } + /// + /// Writes the color profile chunk. + /// + /// The stream to write to. + /// The image meta data. + private void WriteColorProfileChunk(Stream stream, ImageMetadata metaData) + { + if (metaData.IccProfile is null) + { + return; + } + + byte[] iccProfileBytes = metaData.IccProfile.ToByteArray(); + + byte[] compressedData = this.GetZlibCompressedBytes(iccProfileBytes); + int payloadLength = ColorProfileName.Length + compressedData.Length + 2; + using (IMemoryOwner owner = this.memoryAllocator.Allocate(payloadLength)) + { + Span outputBytes = owner.GetSpan(); + PngConstants.Encoding.GetBytes(ColorProfileName).CopyTo(outputBytes); + int bytesWritten = ColorProfileName.Length; + outputBytes[bytesWritten++] = 0; // Null separator. + outputBytes[bytesWritten++] = 0; // Compression. + compressedData.CopyTo(outputBytes.Slice(bytesWritten)); + this.WriteChunk(stream, PngChunkType.EmbeddedColorProfile, outputBytes); + } + } + /// /// Writes a text chunk to the stream. Can be either a tTXt, iTXt or zTXt chunk, /// depending whether the text contains any latin characters or should be compressed. @@ -727,13 +763,12 @@ namespace SixLabors.ImageSharp.Formats.Png } } - if (hasUnicodeCharacters || (!string.IsNullOrWhiteSpace(textData.LanguageTag) || - !string.IsNullOrWhiteSpace(textData.TranslatedKeyword))) + if (hasUnicodeCharacters || (!string.IsNullOrWhiteSpace(textData.LanguageTag) || !string.IsNullOrWhiteSpace(textData.TranslatedKeyword))) { // Write iTXt chunk. byte[] keywordBytes = PngConstants.Encoding.GetBytes(textData.Keyword); byte[] textBytes = textData.Value.Length > this.options.TextCompressionThreshold - ? this.GetCompressedTextBytes(PngConstants.TranslatedEncoding.GetBytes(textData.Value)) + ? this.GetZlibCompressedBytes(PngConstants.TranslatedEncoding.GetBytes(textData.Value)) : PngConstants.TranslatedEncoding.GetBytes(textData.Value); byte[] translatedKeyword = PngConstants.TranslatedEncoding.GetBytes(textData.TranslatedKeyword); @@ -772,18 +807,17 @@ namespace SixLabors.ImageSharp.Formats.Png if (textData.Value.Length > this.options.TextCompressionThreshold) { // Write zTXt chunk. - byte[] compressedData = - this.GetCompressedTextBytes(PngConstants.Encoding.GetBytes(textData.Value)); + byte[] compressedData = this.GetZlibCompressedBytes(PngConstants.Encoding.GetBytes(textData.Value)); int payloadLength = textData.Keyword.Length + compressedData.Length + 2; using (IMemoryOwner owner = this.memoryAllocator.Allocate(payloadLength)) { Span outputBytes = owner.GetSpan(); PngConstants.Encoding.GetBytes(textData.Keyword).CopyTo(outputBytes); int bytesWritten = textData.Keyword.Length; - outputBytes[bytesWritten++] = 0; - outputBytes[bytesWritten++] = 0; + outputBytes[bytesWritten++] = 0; // Null separator. + outputBytes[bytesWritten++] = 0; // Compression. compressedData.CopyTo(outputBytes.Slice(bytesWritten)); - this.WriteChunk(stream, PngChunkType.CompressedText, outputBytes.ToArray()); + this.WriteChunk(stream, PngChunkType.CompressedText, outputBytes); } } else @@ -796,9 +830,8 @@ namespace SixLabors.ImageSharp.Formats.Png PngConstants.Encoding.GetBytes(textData.Keyword).CopyTo(outputBytes); int bytesWritten = textData.Keyword.Length; outputBytes[bytesWritten++] = 0; - PngConstants.Encoding.GetBytes(textData.Value) - .CopyTo(outputBytes.Slice(bytesWritten)); - this.WriteChunk(stream, PngChunkType.Text, outputBytes.ToArray()); + PngConstants.Encoding.GetBytes(textData.Value).CopyTo(outputBytes.Slice(bytesWritten)); + this.WriteChunk(stream, PngChunkType.Text, outputBytes); } } } @@ -808,15 +841,15 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// Compresses a given text using Zlib compression. /// - /// The text bytes to compress. - /// The compressed text byte array. - private byte[] GetCompressedTextBytes(byte[] textBytes) + /// The bytes to compress. + /// The compressed byte array. + private byte[] GetZlibCompressedBytes(byte[] dataBytes) { using (var memoryStream = new MemoryStream()) { using (var deflateStream = new ZlibDeflateStream(this.memoryAllocator, memoryStream, this.options.CompressionLevel)) { - deflateStream.Write(textBytes); + deflateStream.Write(dataBytes); } return memoryStream.ToArray(); diff --git a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs index 58fa5aca8..26bc566d6 100644 --- a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs +++ b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs @@ -240,6 +240,11 @@ namespace SixLabors.ImageSharp.Formats.Png byte[] paletteAlpha) where TPixel : unmanaged, IPixel { + if (palette.IsEmpty) + { + PngThrowHelper.ThrowMissingPalette(); + } + TPixel pixel = default; ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); diff --git a/src/ImageSharp/Formats/Png/PngThrowHelper.cs b/src/ImageSharp/Formats/Png/PngThrowHelper.cs index 8700438bd..ae7d16ec7 100644 --- a/src/ImageSharp/Formats/Png/PngThrowHelper.cs +++ b/src/ImageSharp/Formats/Png/PngThrowHelper.cs @@ -21,6 +21,9 @@ namespace SixLabors.ImageSharp.Formats.Png [MethodImpl(InliningOptions.ColdPath)] public static void ThrowNoData() => throw new InvalidImageContentException("PNG Image does not contain a data chunk"); + [MethodImpl(InliningOptions.ColdPath)] + public static void ThrowMissingPalette() => throw new InvalidImageContentException("PNG Image does not contain a palette chunk"); + [MethodImpl(InliningOptions.ColdPath)] public static void ThrowInvalidChunkType() => throw new InvalidImageContentException("Invalid PNG data."); diff --git a/src/ImageSharp/Formats/Tga/TgaDecoder.cs b/src/ImageSharp/Formats/Tga/TgaDecoder.cs index e06a0ee88..bb0a0d548 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoder.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoder.cs @@ -3,9 +3,6 @@ using System.IO; using System.Threading; -using System.Threading.Tasks; -using SixLabors.ImageSharp.IO; -using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Tga @@ -16,48 +13,25 @@ namespace SixLabors.ImageSharp.Formats.Tga public sealed class TgaDecoder : IImageDecoder, ITgaDecoderOptions, IImageInfoDetector { /// - public Image Decode(Configuration configuration, Stream stream) + public Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { Guard.NotNull(stream, nameof(stream)); var decoder = new TgaDecoderCore(configuration, this); - return decoder.Decode(configuration, stream); + return decoder.Decode(configuration, stream, cancellationToken); } /// - public Image Decode(Configuration configuration, Stream stream) - => this.Decode(configuration, stream); + public Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken) + => this.Decode(configuration, stream, cancellationToken); /// - public Task> DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel - { - Guard.NotNull(stream, nameof(stream)); - - var decoder = new TgaDecoderCore(configuration, this); - return decoder.DecodeAsync(configuration, stream, cancellationToken); - } - - /// - public async Task DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) - => await this.DecodeAsync(configuration, stream, cancellationToken) - .ConfigureAwait(false); - - /// - public IImageInfo Identify(Configuration configuration, Stream stream) - { - Guard.NotNull(stream, nameof(stream)); - - return new TgaDecoderCore(configuration, this).Identify(configuration, stream); - } - - /// - public Task IdentifyAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) + public IImageInfo Identify(Configuration configuration, Stream stream, CancellationToken cancellationToken) { Guard.NotNull(stream, nameof(stream)); - return new TgaDecoderCore(configuration, this).IdentifyAsync(configuration, stream, cancellationToken); + return new TgaDecoderCore(configuration, this).Identify(configuration, stream, cancellationToken); } } } diff --git a/src/ImageSharp/Formats/Tiff/Compression/Compressors/T4BitCompressor.cs b/src/ImageSharp/Formats/Tiff/Compression/Compressors/T4BitCompressor.cs index 30da537eb..d038e9c8b 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Compressors/T4BitCompressor.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Compressors/T4BitCompressor.cs @@ -2,8 +2,6 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Buffers; -using System.Collections.Generic; using System.IO; using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.Memory; @@ -13,222 +11,35 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors /// /// Bitwriter for writing compressed CCITT T4 1D data. /// - internal sealed class T4BitCompressor : TiffBaseCompressor + internal sealed class T4BitCompressor : TiffCcittCompressor { - private const uint WhiteZeroRunTermCode = 0x35; - - private const uint BlackZeroRunTermCode = 0x37; - - private static readonly uint[] MakeupRunLength = - { - 64, 128, 192, 256, 320, 384, 448, 512, 576, 640, 704, 768, 832, 896, 960, 1024, 1088, 1152, 1216, 1280, 1344, 1408, 1472, 1536, 1600, 1664, 1728, 1792, 1856, 1920, 1984, 2048, 2112, 2176, 2240, 2304, 2368, 2432, 2496, 2560 - }; - - private static readonly Dictionary WhiteLen4TermCodes = new Dictionary() - { - { 2, 0x7 }, { 3, 0x8 }, { 4, 0xB }, { 5, 0xC }, { 6, 0xE }, { 7, 0xF } - }; - - private static readonly Dictionary WhiteLen5TermCodes = new Dictionary() - { - { 8, 0x13 }, { 9, 0x14 }, { 10, 0x7 }, { 11, 0x8 } - }; - - private static readonly Dictionary WhiteLen6TermCodes = new Dictionary() - { - { 1, 0x7 }, { 12, 0x8 }, { 13, 0x3 }, { 14, 0x34 }, { 15, 0x35 }, { 16, 0x2A }, { 17, 0x2B } - }; - - private static readonly Dictionary WhiteLen7TermCodes = new Dictionary() - { - { 18, 0x27 }, { 19, 0xC }, { 20, 0x8 }, { 21, 0x17 }, { 22, 0x3 }, { 23, 0x4 }, { 24, 0x28 }, { 25, 0x2B }, { 26, 0x13 }, - { 27, 0x24 }, { 28, 0x18 } - }; - - private static readonly Dictionary WhiteLen8TermCodes = new Dictionary() - { - { 0, WhiteZeroRunTermCode }, { 29, 0x2 }, { 30, 0x3 }, { 31, 0x1A }, { 32, 0x1B }, { 33, 0x12 }, { 34, 0x13 }, { 35, 0x14 }, - { 36, 0x15 }, { 37, 0x16 }, { 38, 0x17 }, { 39, 0x28 }, { 40, 0x29 }, { 41, 0x2A }, { 42, 0x2B }, { 43, 0x2C }, { 44, 0x2D }, - { 45, 0x4 }, { 46, 0x5 }, { 47, 0xA }, { 48, 0xB }, { 49, 0x52 }, { 50, 0x53 }, { 51, 0x54 }, { 52, 0x55 }, { 53, 0x24 }, - { 54, 0x25 }, { 55, 0x58 }, { 56, 0x59 }, { 57, 0x5A }, { 58, 0x5B }, { 59, 0x4A }, { 60, 0x4B }, { 61, 0x32 }, { 62, 0x33 }, - { 63, 0x34 } - }; - - private static readonly Dictionary BlackLen2TermCodes = new Dictionary() - { - { 2, 0x3 }, { 3, 0x2 } - }; - - private static readonly Dictionary BlackLen3TermCodes = new Dictionary() - { - { 1, 0x2 }, { 4, 0x3 } - }; - - private static readonly Dictionary BlackLen4TermCodes = new Dictionary() - { - { 5, 0x3 }, { 6, 0x2 } - }; - - private static readonly Dictionary BlackLen5TermCodes = new Dictionary() - { - { 7, 0x3 } - }; - - private static readonly Dictionary BlackLen6TermCodes = new Dictionary() - { - { 8, 0x5 }, { 9, 0x4 } - }; - - private static readonly Dictionary BlackLen7TermCodes = new Dictionary() - { - { 10, 0x4 }, { 11, 0x5 }, { 12, 0x7 } - }; - - private static readonly Dictionary BlackLen8TermCodes = new Dictionary() - { - { 13, 0x4 }, { 14, 0x7 } - }; - - private static readonly Dictionary BlackLen9TermCodes = new Dictionary() - { - { 15, 0x18 } - }; - - private static readonly Dictionary BlackLen10TermCodes = new Dictionary() - { - { 0, BlackZeroRunTermCode }, { 16, 0x17 }, { 17, 0x18 }, { 18, 0x8 } - }; - - private static readonly Dictionary BlackLen11TermCodes = new Dictionary() - { - { 19, 0x67 }, { 20, 0x68 }, { 21, 0x6C }, { 22, 0x37 }, { 23, 0x28 }, { 24, 0x17 }, { 25, 0x18 } - }; - - private static readonly Dictionary BlackLen12TermCodes = new Dictionary() - { - { 26, 0xCA }, { 27, 0xCB }, { 28, 0xCC }, { 29, 0xCD }, { 30, 0x68 }, { 31, 0x69 }, { 32, 0x6A }, { 33, 0x6B }, { 34, 0xD2 }, - { 35, 0xD3 }, { 36, 0xD4 }, { 37, 0xD5 }, { 38, 0xD6 }, { 39, 0xD7 }, { 40, 0x6C }, { 41, 0x6D }, { 42, 0xDA }, { 43, 0xDB }, - { 44, 0x54 }, { 45, 0x55 }, { 46, 0x56 }, { 47, 0x57 }, { 48, 0x64 }, { 49, 0x65 }, { 50, 0x52 }, { 51, 0x53 }, { 52, 0x24 }, - { 53, 0x37 }, { 54, 0x38 }, { 55, 0x27 }, { 56, 0x28 }, { 57, 0x58 }, { 58, 0x59 }, { 59, 0x2B }, { 60, 0x2C }, { 61, 0x5A }, - { 62, 0x66 }, { 63, 0x67 } - }; - - private static readonly Dictionary WhiteLen5MakeupCodes = new Dictionary() - { - { 64, 0x1B }, { 128, 0x12 } - }; - - private static readonly Dictionary WhiteLen6MakeupCodes = new Dictionary() - { - { 192, 0x17 }, { 1664, 0x18 } - }; - - private static readonly Dictionary WhiteLen8MakeupCodes = new Dictionary() - { - { 320, 0x36 }, { 384, 0x37 }, { 448, 0x64 }, { 512, 0x65 }, { 576, 0x68 }, { 640, 0x67 } - }; - - private static readonly Dictionary WhiteLen7MakeupCodes = new Dictionary() - { - { 256, 0x37 } - }; - - private static readonly Dictionary WhiteLen9MakeupCodes = new Dictionary() - { - { 704, 0xCC }, { 768, 0xCD }, { 832, 0xD2 }, { 896, 0xD3 }, { 960, 0xD4 }, { 1024, 0xD5 }, { 1088, 0xD6 }, - { 1152, 0xD7 }, { 1216, 0xD8 }, { 1280, 0xD9 }, { 1344, 0xDA }, { 1408, 0xDB }, { 1472, 0x98 }, { 1536, 0x99 }, - { 1600, 0x9A }, { 1728, 0x9B } - }; - - private static readonly Dictionary WhiteLen11MakeupCodes = new Dictionary() - { - { 1792, 0x8 }, { 1856, 0xC }, { 1920, 0xD } - }; - - private static readonly Dictionary WhiteLen12MakeupCodes = new Dictionary() - { - { 1984, 0x12 }, { 2048, 0x13 }, { 2112, 0x14 }, { 2176, 0x15 }, { 2240, 0x16 }, { 2304, 0x17 }, { 2368, 0x1C }, - { 2432, 0x1D }, { 2496, 0x1E }, { 2560, 0x1F } - }; - - private static readonly Dictionary BlackLen10MakeupCodes = new Dictionary() - { - { 64, 0xF } - }; - - private static readonly Dictionary BlackLen11MakeupCodes = new Dictionary() - { - { 1792, 0x8 }, { 1856, 0xC }, { 1920, 0xD } - }; - - private static readonly Dictionary BlackLen12MakeupCodes = new Dictionary() - { - { 128, 0xC8 }, { 192, 0xC9 }, { 256, 0x5B }, { 320, 0x33 }, { 384, 0x34 }, { 448, 0x35 }, - { 1984, 0x12 }, { 2048, 0x13 }, { 2112, 0x14 }, { 2176, 0x15 }, { 2240, 0x16 }, { 2304, 0x17 }, { 2368, 0x1C }, - { 2432, 0x1D }, { 2496, 0x1E }, { 2560, 0x1F } - }; - - private static readonly Dictionary BlackLen13MakeupCodes = new Dictionary() - { - { 512, 0x6C }, { 576, 0x6D }, { 640, 0x4A }, { 704, 0x4B }, { 768, 0x4C }, { 832, 0x4D }, { 896, 0x72 }, - { 960, 0x73 }, { 1024, 0x74 }, { 1088, 0x75 }, { 1152, 0x76 }, { 1216, 0x77 }, { 1280, 0x52 }, { 1344, 0x53 }, - { 1408, 0x54 }, { 1472, 0x55 }, { 1536, 0x5A }, { 1600, 0x5B }, { 1664, 0x64 }, { 1728, 0x65 } - }; - /// /// The modified huffman is basically the same as CCITT T4, but without EOL markers and padding at the end of the rows. /// private readonly bool useModifiedHuffman; - private IMemoryOwner compressedDataBuffer; - - private int bytePosition; - - private byte bitPosition; - /// /// Initializes a new instance of the class. /// - /// The output. - /// The allocator. - /// The width. + /// The output stream to write the compressed data. + /// The memory allocator. + /// The width of the image. /// The bits per pixel. /// Indicates if the modified huffman RLE should be used. public T4BitCompressor(Stream output, MemoryAllocator allocator, int width, int bitsPerPixel, bool useModifiedHuffman = false) - : base(output, allocator, width, bitsPerPixel) - { - this.bytePosition = 0; - this.bitPosition = 0; - this.useModifiedHuffman = useModifiedHuffman; - } + : base(output, allocator, width, bitsPerPixel) => this.useModifiedHuffman = useModifiedHuffman; /// public override TiffCompression Method => this.useModifiedHuffman ? TiffCompression.Ccitt1D : TiffCompression.CcittGroup3Fax; - /// - public override void Initialize(int rowsPerStrip) - { - // This is too much memory allocated, but just 1 bit per pixel will not do, if the compression rate is not good. - int maxNeededBytes = this.Width * rowsPerStrip; - this.compressedDataBuffer = this.Allocator.Allocate(maxNeededBytes); - } - /// - /// Writes a image compressed with CCITT T4 to the stream. + /// Writes a image compressed with CCITT T4 to the output buffer. /// /// The pixels as 8-bit gray array. /// The strip height. - public override void CompressStrip(Span pixelsAsGray, int height) + /// The destination for the compressed data. + protected override void CompressStrip(Span pixelsAsGray, int height, Span compressedData) { - DebugGuard.IsTrue(pixelsAsGray.Length / height == this.Width, "Values must be equals"); - DebugGuard.IsTrue(pixelsAsGray.Length % height == 0, "Values must be equals"); - - this.compressedDataBuffer.Clear(); - Span compressedData = this.compressedDataBuffer.GetSpan(); - - this.bytePosition = 0; - this.bitPosition = 0; - if (!this.useModifiedHuffman) { // An EOL code is expected at the start of the data. @@ -315,25 +126,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors this.WriteEndOfLine(compressedData); } - - // Write the compressed data to the stream. - int bytesToWrite = this.bitPosition != 0 ? this.bytePosition + 1 : this.bytePosition; - this.Output.Write(compressedData.Slice(0, bytesToWrite)); } - protected override void Dispose(bool disposing) => this.compressedDataBuffer?.Dispose(); - private void WriteEndOfLine(Span compressedData) { if (this.useModifiedHuffman) { - // Check if padding is necessary. - if (this.bitPosition % 8 != 0) - { - // Skip padding bits, move to next byte. - this.bytePosition++; - this.bitPosition = 0; - } + this.PadByte(); } else { @@ -341,254 +140,5 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors this.WriteCode(12, 1, compressedData); } } - - private void WriteCode(uint codeLength, uint code, Span compressedData) - { - while (codeLength > 0) - { - int bitNumber = (int)codeLength; - bool bit = (code & (1 << (bitNumber - 1))) != 0; - if (bit) - { - BitWriterUtils.WriteBit(compressedData, this.bytePosition, this.bitPosition); - } - else - { - BitWriterUtils.WriteZeroBit(compressedData, this.bytePosition, this.bitPosition); - } - - this.bitPosition++; - if (this.bitPosition == 8) - { - this.bytePosition++; - this.bitPosition = 0; - } - - codeLength--; - } - } - - private uint GetBestFittingMakeupRunLength(uint runLength) - { - for (int i = 0; i < MakeupRunLength.Length - 1; i++) - { - if (MakeupRunLength[i] <= runLength && MakeupRunLength[i + 1] > runLength) - { - return MakeupRunLength[i]; - } - } - - return MakeupRunLength[MakeupRunLength.Length - 1]; - } - - private uint GetTermCode(uint runLength, out uint codeLength, bool isWhiteRun) - { - if (isWhiteRun) - { - return this.GetWhiteTermCode(runLength, out codeLength); - } - - return this.GetBlackTermCode(runLength, out codeLength); - } - - private uint GetMakeupCode(uint runLength, out uint codeLength, bool isWhiteRun) - { - if (isWhiteRun) - { - return this.GetWhiteMakeupCode(runLength, out codeLength); - } - - return this.GetBlackMakeupCode(runLength, out codeLength); - } - - private uint GetWhiteMakeupCode(uint runLength, out uint codeLength) - { - codeLength = 0; - - if (WhiteLen5MakeupCodes.ContainsKey(runLength)) - { - codeLength = 5; - return WhiteLen5MakeupCodes[runLength]; - } - - if (WhiteLen6MakeupCodes.ContainsKey(runLength)) - { - codeLength = 6; - return WhiteLen6MakeupCodes[runLength]; - } - - if (WhiteLen7MakeupCodes.ContainsKey(runLength)) - { - codeLength = 7; - return WhiteLen7MakeupCodes[runLength]; - } - - if (WhiteLen8MakeupCodes.ContainsKey(runLength)) - { - codeLength = 8; - return WhiteLen8MakeupCodes[runLength]; - } - - if (WhiteLen9MakeupCodes.ContainsKey(runLength)) - { - codeLength = 9; - return WhiteLen9MakeupCodes[runLength]; - } - - if (WhiteLen11MakeupCodes.ContainsKey(runLength)) - { - codeLength = 11; - return WhiteLen11MakeupCodes[runLength]; - } - - if (WhiteLen12MakeupCodes.ContainsKey(runLength)) - { - codeLength = 12; - return WhiteLen12MakeupCodes[runLength]; - } - - return 0; - } - - private uint GetBlackMakeupCode(uint runLength, out uint codeLength) - { - codeLength = 0; - - if (BlackLen10MakeupCodes.ContainsKey(runLength)) - { - codeLength = 10; - return BlackLen10MakeupCodes[runLength]; - } - - if (BlackLen11MakeupCodes.ContainsKey(runLength)) - { - codeLength = 11; - return BlackLen11MakeupCodes[runLength]; - } - - if (BlackLen12MakeupCodes.ContainsKey(runLength)) - { - codeLength = 12; - return BlackLen12MakeupCodes[runLength]; - } - - if (BlackLen13MakeupCodes.ContainsKey(runLength)) - { - codeLength = 13; - return BlackLen13MakeupCodes[runLength]; - } - - return 0; - } - - private uint GetWhiteTermCode(uint runLength, out uint codeLength) - { - codeLength = 0; - - if (WhiteLen4TermCodes.ContainsKey(runLength)) - { - codeLength = 4; - return WhiteLen4TermCodes[runLength]; - } - - if (WhiteLen5TermCodes.ContainsKey(runLength)) - { - codeLength = 5; - return WhiteLen5TermCodes[runLength]; - } - - if (WhiteLen6TermCodes.ContainsKey(runLength)) - { - codeLength = 6; - return WhiteLen6TermCodes[runLength]; - } - - if (WhiteLen7TermCodes.ContainsKey(runLength)) - { - codeLength = 7; - return WhiteLen7TermCodes[runLength]; - } - - if (WhiteLen8TermCodes.ContainsKey(runLength)) - { - codeLength = 8; - return WhiteLen8TermCodes[runLength]; - } - - return 0; - } - - private uint GetBlackTermCode(uint runLength, out uint codeLength) - { - codeLength = 0; - - if (BlackLen2TermCodes.ContainsKey(runLength)) - { - codeLength = 2; - return BlackLen2TermCodes[runLength]; - } - - if (BlackLen3TermCodes.ContainsKey(runLength)) - { - codeLength = 3; - return BlackLen3TermCodes[runLength]; - } - - if (BlackLen4TermCodes.ContainsKey(runLength)) - { - codeLength = 4; - return BlackLen4TermCodes[runLength]; - } - - if (BlackLen5TermCodes.ContainsKey(runLength)) - { - codeLength = 5; - return BlackLen5TermCodes[runLength]; - } - - if (BlackLen6TermCodes.ContainsKey(runLength)) - { - codeLength = 6; - return BlackLen6TermCodes[runLength]; - } - - if (BlackLen7TermCodes.ContainsKey(runLength)) - { - codeLength = 7; - return BlackLen7TermCodes[runLength]; - } - - if (BlackLen8TermCodes.ContainsKey(runLength)) - { - codeLength = 8; - return BlackLen8TermCodes[runLength]; - } - - if (BlackLen9TermCodes.ContainsKey(runLength)) - { - codeLength = 9; - return BlackLen9TermCodes[runLength]; - } - - if (BlackLen10TermCodes.ContainsKey(runLength)) - { - codeLength = 10; - return BlackLen10TermCodes[runLength]; - } - - if (BlackLen11TermCodes.ContainsKey(runLength)) - { - codeLength = 11; - return BlackLen11TermCodes[runLength]; - } - - if (BlackLen12TermCodes.ContainsKey(runLength)) - { - codeLength = 12; - return BlackLen12TermCodes[runLength]; - } - - return 0; - } } } diff --git a/src/ImageSharp/Formats/Tiff/Compression/Compressors/T6BitCompressor.cs b/src/ImageSharp/Formats/Tiff/Compression/Compressors/T6BitCompressor.cs new file mode 100644 index 000000000..9e03d2764 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/Compressors/T6BitCompressor.cs @@ -0,0 +1,201 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.IO; +using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors +{ + /// + /// Bitwriter for writing compressed CCITT T6 2D data. + /// + internal sealed class T6BitCompressor : TiffCcittCompressor + { + /// + /// Vertical codes from -3 to +3. + /// + private static readonly (uint Length, uint Code)[] VerticalCodes = + { + (7u, 3u), + (6u, 3u), + (3u, 3u), + (1u, 1u), + (3u, 2u), + (6u, 2u), + (7u, 2u) + }; + + private IMemoryOwner referenceLineBuffer; + + /// + /// Initializes a new instance of the class. + /// + /// The output stream to write the compressed data. + /// The memory allocator. + /// The width of the image. + /// The bits per pixel. + public T6BitCompressor(Stream output, MemoryAllocator allocator, int width, int bitsPerPixel) + : base(output, allocator, width, bitsPerPixel) + { + } + + /// + public override TiffCompression Method => TiffCompression.CcittGroup4Fax; + + /// + /// Writes a image compressed with CCITT T6 to the output buffer. + /// + /// The pixels as 8-bit gray array. + /// The strip height. + /// The destination for the compressed data. + protected override void CompressStrip(Span pixelsAsGray, int height, Span compressedData) + { + // Initial reference line is all white. + Span referenceLine = this.referenceLineBuffer.GetSpan(); + referenceLine.Fill(0xff); + + for (int y = 0; y < height; y++) + { + Span row = pixelsAsGray.Slice(y * this.Width, this.Width); + uint a0 = 0; + uint a1 = row[0] == 0 ? 0 : this.FindRunEnd(row, 0); + uint b1 = referenceLine[0] == 0 ? 0 : this.FindRunEnd(referenceLine, 0); + + while (true) + { + uint b2 = this.FindRunEnd(referenceLine, b1); + if (b2 < a1) + { + // Pass mode. + this.WriteCode(4, 1, compressedData); + a0 = b2; + } + else + { + int d = int.MaxValue; + if ((b1 >= a1) && (b1 - a1 <= 3)) + { + d = (int)(b1 - a1); + } + else if ((b1 < a1) && (a1 - b1 <= 3)) + { + d = -(int)(a1 - b1); + } + + if ((d >= -3) && (d <= 3)) + { + // Vertical mode. + (uint length, uint code) = VerticalCodes[d + 3]; + this.WriteCode(length, code, compressedData); + a0 = a1; + } + else + { + // Horizontal mode. + this.WriteCode(3, 1, compressedData); + + uint a2 = this.FindRunEnd(row, a1); + if ((a0 + a1 == 0) || (row[(int)a0] != 0)) + { + this.WriteRun(a1 - a0, true, compressedData); + this.WriteRun(a2 - a1, false, compressedData); + } + else + { + this.WriteRun(a1 - a0, false, compressedData); + this.WriteRun(a2 - a1, true, compressedData); + } + + a0 = a2; + } + } + + if (a0 >= row.Length) + { + break; + } + + byte thisPixel = row[(int)a0]; + a1 = this.FindRunEnd(row, a0, thisPixel); + b1 = this.FindRunEnd(referenceLine, a0, (byte)~thisPixel); + b1 = this.FindRunEnd(referenceLine, b1, thisPixel); + } + + // This row is now the reference line. + row.CopyTo(referenceLine); + } + + this.WriteCode(12, 1, compressedData); + this.WriteCode(12, 1, compressedData); + } + + /// + protected override void Dispose(bool disposing) + { + this.referenceLineBuffer?.Dispose(); + base.Dispose(disposing); + } + + /// + /// Finds the end of a pixel run. + /// + /// The row of pixels to examine. + /// The index of the first pixel in to examine. + /// Color of pixels in the run. If not specified, the color at + /// will be used. + /// The index of the first pixel at or after + /// that does not match , or the length of , + /// whichever comes first. + private uint FindRunEnd(Span row, uint startIndex, byte? color = null) + { + if (startIndex >= row.Length) + { + return (uint)row.Length; + } + + byte colorValue = color.GetValueOrDefault(row[(int)startIndex]); + for (int i = (int)startIndex; i < row.Length; i++) + { + if (row[i] != colorValue) + { + return (uint)i; + } + } + + return (uint)row.Length; + } + + /// + public override void Initialize(int rowsPerStrip) + { + base.Initialize(rowsPerStrip); + this.referenceLineBuffer = this.Allocator.Allocate(this.Width); + } + + /// + /// Writes a run to the output buffer. + /// + /// The length of the run. + /// If true the run is white pixels, + /// if false the run is black pixels. + /// The destination to write the run to. + private void WriteRun(uint runLength, bool isWhiteRun, Span compressedData) + { + uint code; + uint codeLength; + while (runLength > 63) + { + uint makeupLength = this.GetBestFittingMakeupRunLength(runLength); + code = this.GetMakeupCode(makeupLength, out codeLength, isWhiteRun); + this.WriteCode(codeLength, code, compressedData); + runLength -= makeupLength; + } + + code = this.GetTermCode(runLength, out codeLength, isWhiteRun); + this.WriteCode(codeLength, code, compressedData); + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffCcittCompressor.cs b/src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffCcittCompressor.cs new file mode 100644 index 000000000..316610621 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffCcittCompressor.cs @@ -0,0 +1,536 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.IO; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors +{ + /// + /// Common functionality for CCITT T4 and T6 Compression + /// + internal abstract class TiffCcittCompressor : TiffBaseCompressor + { + protected const uint WhiteZeroRunTermCode = 0x35; + + protected const uint BlackZeroRunTermCode = 0x37; + + private static readonly uint[] MakeupRunLength = + { + 64, 128, 192, 256, 320, 384, 448, 512, 576, 640, 704, 768, 832, 896, 960, 1024, 1088, 1152, 1216, 1280, 1344, 1408, 1472, 1536, 1600, 1664, 1728, 1792, 1856, 1920, 1984, 2048, 2112, 2176, 2240, 2304, 2368, 2432, 2496, 2560 + }; + + private static readonly Dictionary WhiteLen4TermCodes = new Dictionary() + { + { 2, 0x7 }, { 3, 0x8 }, { 4, 0xB }, { 5, 0xC }, { 6, 0xE }, { 7, 0xF } + }; + + private static readonly Dictionary WhiteLen5TermCodes = new Dictionary() + { + { 8, 0x13 }, { 9, 0x14 }, { 10, 0x7 }, { 11, 0x8 } + }; + + private static readonly Dictionary WhiteLen6TermCodes = new Dictionary() + { + { 1, 0x7 }, { 12, 0x8 }, { 13, 0x3 }, { 14, 0x34 }, { 15, 0x35 }, { 16, 0x2A }, { 17, 0x2B } + }; + + private static readonly Dictionary WhiteLen7TermCodes = new Dictionary() + { + { 18, 0x27 }, { 19, 0xC }, { 20, 0x8 }, { 21, 0x17 }, { 22, 0x3 }, { 23, 0x4 }, { 24, 0x28 }, { 25, 0x2B }, { 26, 0x13 }, + { 27, 0x24 }, { 28, 0x18 } + }; + + private static readonly Dictionary WhiteLen8TermCodes = new Dictionary() + { + { 0, WhiteZeroRunTermCode }, { 29, 0x2 }, { 30, 0x3 }, { 31, 0x1A }, { 32, 0x1B }, { 33, 0x12 }, { 34, 0x13 }, { 35, 0x14 }, + { 36, 0x15 }, { 37, 0x16 }, { 38, 0x17 }, { 39, 0x28 }, { 40, 0x29 }, { 41, 0x2A }, { 42, 0x2B }, { 43, 0x2C }, { 44, 0x2D }, + { 45, 0x4 }, { 46, 0x5 }, { 47, 0xA }, { 48, 0xB }, { 49, 0x52 }, { 50, 0x53 }, { 51, 0x54 }, { 52, 0x55 }, { 53, 0x24 }, + { 54, 0x25 }, { 55, 0x58 }, { 56, 0x59 }, { 57, 0x5A }, { 58, 0x5B }, { 59, 0x4A }, { 60, 0x4B }, { 61, 0x32 }, { 62, 0x33 }, + { 63, 0x34 } + }; + + private static readonly Dictionary BlackLen2TermCodes = new Dictionary() + { + { 2, 0x3 }, { 3, 0x2 } + }; + + private static readonly Dictionary BlackLen3TermCodes = new Dictionary() + { + { 1, 0x2 }, { 4, 0x3 } + }; + + private static readonly Dictionary BlackLen4TermCodes = new Dictionary() + { + { 5, 0x3 }, { 6, 0x2 } + }; + + private static readonly Dictionary BlackLen5TermCodes = new Dictionary() + { + { 7, 0x3 } + }; + + private static readonly Dictionary BlackLen6TermCodes = new Dictionary() + { + { 8, 0x5 }, { 9, 0x4 } + }; + + private static readonly Dictionary BlackLen7TermCodes = new Dictionary() + { + { 10, 0x4 }, { 11, 0x5 }, { 12, 0x7 } + }; + + private static readonly Dictionary BlackLen8TermCodes = new Dictionary() + { + { 13, 0x4 }, { 14, 0x7 } + }; + + private static readonly Dictionary BlackLen9TermCodes = new Dictionary() + { + { 15, 0x18 } + }; + + private static readonly Dictionary BlackLen10TermCodes = new Dictionary() + { + { 0, BlackZeroRunTermCode }, { 16, 0x17 }, { 17, 0x18 }, { 18, 0x8 } + }; + + private static readonly Dictionary BlackLen11TermCodes = new Dictionary() + { + { 19, 0x67 }, { 20, 0x68 }, { 21, 0x6C }, { 22, 0x37 }, { 23, 0x28 }, { 24, 0x17 }, { 25, 0x18 } + }; + + private static readonly Dictionary BlackLen12TermCodes = new Dictionary() + { + { 26, 0xCA }, { 27, 0xCB }, { 28, 0xCC }, { 29, 0xCD }, { 30, 0x68 }, { 31, 0x69 }, { 32, 0x6A }, { 33, 0x6B }, { 34, 0xD2 }, + { 35, 0xD3 }, { 36, 0xD4 }, { 37, 0xD5 }, { 38, 0xD6 }, { 39, 0xD7 }, { 40, 0x6C }, { 41, 0x6D }, { 42, 0xDA }, { 43, 0xDB }, + { 44, 0x54 }, { 45, 0x55 }, { 46, 0x56 }, { 47, 0x57 }, { 48, 0x64 }, { 49, 0x65 }, { 50, 0x52 }, { 51, 0x53 }, { 52, 0x24 }, + { 53, 0x37 }, { 54, 0x38 }, { 55, 0x27 }, { 56, 0x28 }, { 57, 0x58 }, { 58, 0x59 }, { 59, 0x2B }, { 60, 0x2C }, { 61, 0x5A }, + { 62, 0x66 }, { 63, 0x67 } + }; + + private static readonly Dictionary WhiteLen5MakeupCodes = new Dictionary() + { + { 64, 0x1B }, { 128, 0x12 } + }; + + private static readonly Dictionary WhiteLen6MakeupCodes = new Dictionary() + { + { 192, 0x17 }, { 1664, 0x18 } + }; + + private static readonly Dictionary WhiteLen8MakeupCodes = new Dictionary() + { + { 320, 0x36 }, { 384, 0x37 }, { 448, 0x64 }, { 512, 0x65 }, { 576, 0x68 }, { 640, 0x67 } + }; + + private static readonly Dictionary WhiteLen7MakeupCodes = new Dictionary() + { + { 256, 0x37 } + }; + + private static readonly Dictionary WhiteLen9MakeupCodes = new Dictionary() + { + { 704, 0xCC }, { 768, 0xCD }, { 832, 0xD2 }, { 896, 0xD3 }, { 960, 0xD4 }, { 1024, 0xD5 }, { 1088, 0xD6 }, + { 1152, 0xD7 }, { 1216, 0xD8 }, { 1280, 0xD9 }, { 1344, 0xDA }, { 1408, 0xDB }, { 1472, 0x98 }, { 1536, 0x99 }, + { 1600, 0x9A }, { 1728, 0x9B } + }; + + private static readonly Dictionary WhiteLen11MakeupCodes = new Dictionary() + { + { 1792, 0x8 }, { 1856, 0xC }, { 1920, 0xD } + }; + + private static readonly Dictionary WhiteLen12MakeupCodes = new Dictionary() + { + { 1984, 0x12 }, { 2048, 0x13 }, { 2112, 0x14 }, { 2176, 0x15 }, { 2240, 0x16 }, { 2304, 0x17 }, { 2368, 0x1C }, + { 2432, 0x1D }, { 2496, 0x1E }, { 2560, 0x1F } + }; + + private static readonly Dictionary BlackLen10MakeupCodes = new Dictionary() + { + { 64, 0xF } + }; + + private static readonly Dictionary BlackLen11MakeupCodes = new Dictionary() + { + { 1792, 0x8 }, { 1856, 0xC }, { 1920, 0xD } + }; + + private static readonly Dictionary BlackLen12MakeupCodes = new Dictionary() + { + { 128, 0xC8 }, { 192, 0xC9 }, { 256, 0x5B }, { 320, 0x33 }, { 384, 0x34 }, { 448, 0x35 }, + { 1984, 0x12 }, { 2048, 0x13 }, { 2112, 0x14 }, { 2176, 0x15 }, { 2240, 0x16 }, { 2304, 0x17 }, { 2368, 0x1C }, + { 2432, 0x1D }, { 2496, 0x1E }, { 2560, 0x1F } + }; + + private static readonly Dictionary BlackLen13MakeupCodes = new Dictionary() + { + { 512, 0x6C }, { 576, 0x6D }, { 640, 0x4A }, { 704, 0x4B }, { 768, 0x4C }, { 832, 0x4D }, { 896, 0x72 }, + { 960, 0x73 }, { 1024, 0x74 }, { 1088, 0x75 }, { 1152, 0x76 }, { 1216, 0x77 }, { 1280, 0x52 }, { 1344, 0x53 }, + { 1408, 0x54 }, { 1472, 0x55 }, { 1536, 0x5A }, { 1600, 0x5B }, { 1664, 0x64 }, { 1728, 0x65 } + }; + + private int bytePosition; + + private byte bitPosition; + + private IMemoryOwner compressedDataBuffer; + + /// + /// Initializes a new instance of the class. + /// + /// The output. + /// The allocator. + /// The width. + /// The bits per pixel. + protected TiffCcittCompressor(Stream output, MemoryAllocator allocator, int width, int bitsPerPixel) + : base(output, allocator, width, bitsPerPixel) + { + DebugGuard.IsTrue(bitsPerPixel == 1, nameof(bitsPerPixel), "CCITT compression requires one bit per pixel"); + this.bytePosition = 0; + this.bitPosition = 0; + } + + private uint GetWhiteMakeupCode(uint runLength, out uint codeLength) + { + codeLength = 0; + + if (WhiteLen5MakeupCodes.ContainsKey(runLength)) + { + codeLength = 5; + return WhiteLen5MakeupCodes[runLength]; + } + + if (WhiteLen6MakeupCodes.ContainsKey(runLength)) + { + codeLength = 6; + return WhiteLen6MakeupCodes[runLength]; + } + + if (WhiteLen7MakeupCodes.ContainsKey(runLength)) + { + codeLength = 7; + return WhiteLen7MakeupCodes[runLength]; + } + + if (WhiteLen8MakeupCodes.ContainsKey(runLength)) + { + codeLength = 8; + return WhiteLen8MakeupCodes[runLength]; + } + + if (WhiteLen9MakeupCodes.ContainsKey(runLength)) + { + codeLength = 9; + return WhiteLen9MakeupCodes[runLength]; + } + + if (WhiteLen11MakeupCodes.ContainsKey(runLength)) + { + codeLength = 11; + return WhiteLen11MakeupCodes[runLength]; + } + + if (WhiteLen12MakeupCodes.ContainsKey(runLength)) + { + codeLength = 12; + return WhiteLen12MakeupCodes[runLength]; + } + + return 0; + } + + private uint GetBlackMakeupCode(uint runLength, out uint codeLength) + { + codeLength = 0; + + if (BlackLen10MakeupCodes.ContainsKey(runLength)) + { + codeLength = 10; + return BlackLen10MakeupCodes[runLength]; + } + + if (BlackLen11MakeupCodes.ContainsKey(runLength)) + { + codeLength = 11; + return BlackLen11MakeupCodes[runLength]; + } + + if (BlackLen12MakeupCodes.ContainsKey(runLength)) + { + codeLength = 12; + return BlackLen12MakeupCodes[runLength]; + } + + if (BlackLen13MakeupCodes.ContainsKey(runLength)) + { + codeLength = 13; + return BlackLen13MakeupCodes[runLength]; + } + + return 0; + } + + private uint GetWhiteTermCode(uint runLength, out uint codeLength) + { + codeLength = 0; + + if (WhiteLen4TermCodes.ContainsKey(runLength)) + { + codeLength = 4; + return WhiteLen4TermCodes[runLength]; + } + + if (WhiteLen5TermCodes.ContainsKey(runLength)) + { + codeLength = 5; + return WhiteLen5TermCodes[runLength]; + } + + if (WhiteLen6TermCodes.ContainsKey(runLength)) + { + codeLength = 6; + return WhiteLen6TermCodes[runLength]; + } + + if (WhiteLen7TermCodes.ContainsKey(runLength)) + { + codeLength = 7; + return WhiteLen7TermCodes[runLength]; + } + + if (WhiteLen8TermCodes.ContainsKey(runLength)) + { + codeLength = 8; + return WhiteLen8TermCodes[runLength]; + } + + return 0; + } + + private uint GetBlackTermCode(uint runLength, out uint codeLength) + { + codeLength = 0; + + if (BlackLen2TermCodes.ContainsKey(runLength)) + { + codeLength = 2; + return BlackLen2TermCodes[runLength]; + } + + if (BlackLen3TermCodes.ContainsKey(runLength)) + { + codeLength = 3; + return BlackLen3TermCodes[runLength]; + } + + if (BlackLen4TermCodes.ContainsKey(runLength)) + { + codeLength = 4; + return BlackLen4TermCodes[runLength]; + } + + if (BlackLen5TermCodes.ContainsKey(runLength)) + { + codeLength = 5; + return BlackLen5TermCodes[runLength]; + } + + if (BlackLen6TermCodes.ContainsKey(runLength)) + { + codeLength = 6; + return BlackLen6TermCodes[runLength]; + } + + if (BlackLen7TermCodes.ContainsKey(runLength)) + { + codeLength = 7; + return BlackLen7TermCodes[runLength]; + } + + if (BlackLen8TermCodes.ContainsKey(runLength)) + { + codeLength = 8; + return BlackLen8TermCodes[runLength]; + } + + if (BlackLen9TermCodes.ContainsKey(runLength)) + { + codeLength = 9; + return BlackLen9TermCodes[runLength]; + } + + if (BlackLen10TermCodes.ContainsKey(runLength)) + { + codeLength = 10; + return BlackLen10TermCodes[runLength]; + } + + if (BlackLen11TermCodes.ContainsKey(runLength)) + { + codeLength = 11; + return BlackLen11TermCodes[runLength]; + } + + if (BlackLen12TermCodes.ContainsKey(runLength)) + { + codeLength = 12; + return BlackLen12TermCodes[runLength]; + } + + return 0; + } + + /// + /// Gets the best makeup run length for a given run length + /// + /// A run length needing a makeup code + /// The makeup length for . + protected uint GetBestFittingMakeupRunLength(uint runLength) + { + DebugGuard.MustBeGreaterThanOrEqualTo(runLength, MakeupRunLength[0], nameof(runLength)); + + for (int i = 0; i < MakeupRunLength.Length - 1; i++) + { + if (MakeupRunLength[i] <= runLength && MakeupRunLength[i + 1] > runLength) + { + return MakeupRunLength[i]; + } + } + + return MakeupRunLength[MakeupRunLength.Length - 1]; + } + + /// + /// Gets the terminating code for a run length. + /// + /// The run length to get the terminating code for. + /// The length of the terminating code. + /// If true, the run is of white pixels. + /// If false the run is of black pixels + /// The terminating code for a run of length + protected uint GetTermCode(uint runLength, out uint codeLength, bool isWhiteRun) + { + if (isWhiteRun) + { + return this.GetWhiteTermCode(runLength, out codeLength); + } + + return this.GetBlackTermCode(runLength, out codeLength); + } + + /// + /// Gets the makeup code for a run length. + /// + /// The run length to get the makeup code for. + /// The length of the makeup code. + /// If true, the run is of white pixels. + /// If false the run is of black pixels + /// The makeup code for a run of length + protected uint GetMakeupCode(uint runLength, out uint codeLength, bool isWhiteRun) + { + if (isWhiteRun) + { + return this.GetWhiteMakeupCode(runLength, out codeLength); + } + + return this.GetBlackMakeupCode(runLength, out codeLength); + } + + /// + /// Pads output to the next byte + /// + /// + /// If the output is not currently on a byte boundary, + /// zero-pad it to the next byte + /// + protected void PadByte() + { + // Check if padding is necessary. + if (this.bitPosition % 8 != 0) + { + // Skip padding bits, move to next byte. + this.bytePosition++; + this.bitPosition = 0; + } + } + + /// + /// Writes a code to the output. + /// + /// The length of the code to write. + /// The code to be written. + /// The destination buffer to write the code to. + protected void WriteCode(uint codeLength, uint code, Span compressedData) + { + while (codeLength > 0) + { + int bitNumber = (int)codeLength; + bool bit = (code & (1 << (bitNumber - 1))) != 0; + if (bit) + { + BitWriterUtils.WriteBit(compressedData, this.bytePosition, this.bitPosition); + } + else + { + BitWriterUtils.WriteZeroBit(compressedData, this.bytePosition, this.bitPosition); + } + + this.bitPosition++; + if (this.bitPosition == 8) + { + this.bytePosition++; + this.bitPosition = 0; + } + + codeLength--; + } + } + + /// + /// Writes a image compressed with CCITT T6 to the stream. + /// + /// The pixels as 8-bit gray array. + /// The strip height. + public override void CompressStrip(Span pixelsAsGray, int height) + { + DebugGuard.IsTrue(pixelsAsGray.Length / height == this.Width, "Values must be equals"); + DebugGuard.IsTrue(pixelsAsGray.Length % height == 0, "Values must be equals"); + + this.compressedDataBuffer.Clear(); + Span compressedData = this.compressedDataBuffer.GetSpan(); + + this.bytePosition = 0; + this.bitPosition = 0; + + this.CompressStrip(pixelsAsGray, height, compressedData); + + // Write the compressed data to the stream. + int bytesToWrite = this.bitPosition != 0 ? this.bytePosition + 1 : this.bytePosition; + this.Output.Write(compressedData.Slice(0, bytesToWrite)); + } + + /// + /// Compress a data strip + /// + /// The pixels as 8-bit gray array. + /// The strip height. + /// The destination for the compressed data. + protected abstract void CompressStrip(Span pixelsAsGray, int height, Span compressedData); + + /// + protected override void Dispose(bool disposing) => this.compressedDataBuffer?.Dispose(); + + /// + public override void Initialize(int rowsPerStrip) + { + // This is too much memory allocated, but just 1 bit per pixel will not do, if the compression rate is not good. + int maxNeededBytes = this.Width * rowsPerStrip; + this.compressedDataBuffer = this.Allocator.Allocate(maxNeededBytes); + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/GrayJpegSpectralConverter.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/GrayJpegSpectralConverter.cs new file mode 100644 index 000000000..5b793c35d --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/GrayJpegSpectralConverter.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; +using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors +{ + /// + /// Spectral converter for gray TIFF's which use the JPEG compression. + /// + /// The type of the pixel. + internal sealed class GrayJpegSpectralConverter : SpectralConverter + where TPixel : unmanaged, IPixel + { + /// + /// Initializes a new instance of the class. + /// + /// The configuration. + public GrayJpegSpectralConverter(Configuration configuration) + : base(configuration) + { + } + + /// + protected override JpegColorConverterBase GetColorConverter(JpegFrame frame, IRawJpegData jpegData) => JpegColorConverterBase.GetConverter(JpegColorSpace.Grayscale, frame.Precision); + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs index ce7820ccf..88dbcb882 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs @@ -9,7 +9,6 @@ using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors @@ -55,17 +54,43 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors { using var jpegDecoder = new JpegDecoderCore(this.configuration, new JpegDecoder()); - // If the PhotometricInterpretation is YCbCr we explicitly assume the JPEG data is in RGB color space. - // There seems no other way to determine that the JPEG data is RGB colorspace (no APP14 marker, componentId's are not RGB). - using SpectralConverter spectralConverter = this.photometricInterpretation == TiffPhotometricInterpretation.YCbCr ? - new RgbJpegSpectralConverter(this.configuration) : new SpectralConverter(this.configuration); - var scanDecoder = new HuffmanScanDecoder(stream, spectralConverter, CancellationToken.None); - jpegDecoder.LoadTables(this.jpegTables, scanDecoder); - scanDecoder.ResetInterval = 0; - jpegDecoder.ParseStream(stream, scanDecoder, CancellationToken.None); + switch (this.photometricInterpretation) + { + case TiffPhotometricInterpretation.BlackIsZero: + case TiffPhotometricInterpretation.WhiteIsZero: + { + using SpectralConverter spectralConverterGray = new GrayJpegSpectralConverter(this.configuration); + var scanDecoderGray = new HuffmanScanDecoder(stream, spectralConverterGray, CancellationToken.None); + jpegDecoder.LoadTables(this.jpegTables, scanDecoderGray); + jpegDecoder.ParseStream(stream, spectralConverterGray, CancellationToken.None); - // TODO: Should we pass through the CancellationToken from the tiff decoder? - CopyImageBytesToBuffer(buffer, spectralConverter.GetPixelBuffer(CancellationToken.None)); + // TODO: Should we pass through the CancellationToken from the tiff decoder? + using var decompressedBuffer = spectralConverterGray.GetPixelBuffer(CancellationToken.None); + CopyImageBytesToBuffer(buffer, decompressedBuffer); + break; + } + + // If the PhotometricInterpretation is YCbCr we explicitly assume the JPEG data is in RGB color space. + // There seems no other way to determine that the JPEG data is RGB colorspace (no APP14 marker, componentId's are not RGB). + case TiffPhotometricInterpretation.YCbCr: + case TiffPhotometricInterpretation.Rgb: + { + using SpectralConverter spectralConverter = this.photometricInterpretation == TiffPhotometricInterpretation.YCbCr ? + new RgbJpegSpectralConverter(this.configuration) : new SpectralConverter(this.configuration); + var scanDecoder = new HuffmanScanDecoder(stream, spectralConverter, CancellationToken.None); + jpegDecoder.LoadTables(this.jpegTables, scanDecoder); + jpegDecoder.ParseStream(stream, spectralConverter, CancellationToken.None); + + // TODO: Should we pass through the CancellationToken from the tiff decoder? + using var decompressedBuffer = spectralConverter.GetPixelBuffer(CancellationToken.None); + CopyImageBytesToBuffer(buffer, decompressedBuffer); + break; + } + + default: + TiffThrowHelper.ThrowNotSupported($"Jpeg compressed tiff with photometric interpretation {this.photometricInterpretation} is not supported"); + break; + } } else { @@ -86,6 +111,18 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors } } + private static void CopyImageBytesToBuffer(Span buffer, Buffer2D pixelBuffer) + { + int offset = 0; + for (int y = 0; y < pixelBuffer.Height; y++) + { + Span pixelRowSpan = pixelBuffer.DangerousGetRowSpan(y); + Span rgbBytes = MemoryMarshal.AsBytes(pixelRowSpan); + rgbBytes.CopyTo(buffer.Slice(offset)); + offset += rgbBytes.Length; + } + } + /// protected override void Dispose(bool disposing) { diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/RgbJpegSpectralConverter.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/RgbJpegSpectralConverter.cs index 001480542..a83518064 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/RgbJpegSpectralConverter.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/RgbJpegSpectralConverter.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. -using System.Threading; using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters; using SixLabors.ImageSharp.PixelFormats; diff --git a/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs b/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs index e2dbc6ca9..97bce2b2d 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/HorizontalPredictor.cs @@ -22,7 +22,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression /// Buffer with decompressed pixel data. /// The width of the image or strip. /// The color type of the pixel data. - /// if set to true decodes the pixel data as big endian, otherwise as little endian. + /// If set to true decodes the pixel data as big endian, otherwise as little endian. public static void Undo(Span pixelBytes, int width, TiffColorType colorType, bool isBigEndian) { switch (colorType) @@ -43,12 +43,21 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression case TiffColorType.Rgb888: UndoRgb24Bit(pixelBytes, width); break; + case TiffColorType.Rgba8888: + UndoRgba32Bit(pixelBytes, width); + break; case TiffColorType.Rgb161616: UndoRgb48Bit(pixelBytes, width, isBigEndian); break; + case TiffColorType.Rgba16161616: + UndoRgba64Bit(pixelBytes, width, isBigEndian); + break; case TiffColorType.Rgb323232: UndoRgb96Bit(pixelBytes, width, isBigEndian); break; + case TiffColorType.Rgba32323232: + UndoRgba128Bit(pixelBytes, width, isBigEndian); + break; } } @@ -243,6 +252,33 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression } } + private static void UndoRgba32Bit(Span pixelBytes, int width) + { + int rowBytesCount = width * 4; + int height = pixelBytes.Length / rowBytesCount; + for (int y = 0; y < height; y++) + { + Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + Span rowRgb = MemoryMarshal.Cast(rowBytes).Slice(0, width); + ref Rgba32 rowRgbBase = ref MemoryMarshal.GetReference(rowRgb); + byte r = rowRgbBase.R; + byte g = rowRgbBase.G; + byte b = rowRgbBase.B; + byte a = rowRgbBase.A; + + for (int x = 1; x < rowRgb.Length; x++) + { + ref Rgba32 pixel = ref rowRgb[x]; + r += pixel.R; + g += pixel.G; + b += pixel.B; + a += pixel.A; + var rgb = new Rgba32(r, g, b, a); + pixel.FromRgba32(rgb); + } + } + } + private static void UndoRgb48Bit(Span pixelBytes, int width, bool isBigEndian) { int rowBytesCount = width * 6; @@ -319,6 +355,98 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression } } + private static void UndoRgba64Bit(Span pixelBytes, int width, bool isBigEndian) + { + int rowBytesCount = width * 8; + int height = pixelBytes.Length / rowBytesCount; + if (isBigEndian) + { + for (int y = 0; y < height; y++) + { + int offset = 0; + Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + ushort r = TiffUtils.ConvertToUShortBigEndian(rowBytes.Slice(offset, 2)); + offset += 2; + ushort g = TiffUtils.ConvertToUShortBigEndian(rowBytes.Slice(offset, 2)); + offset += 2; + ushort b = TiffUtils.ConvertToUShortBigEndian(rowBytes.Slice(offset, 2)); + offset += 2; + ushort a = TiffUtils.ConvertToUShortBigEndian(rowBytes.Slice(offset, 2)); + offset += 2; + + for (int x = 1; x < width; x++) + { + Span rowSpan = rowBytes.Slice(offset, 2); + ushort deltaR = TiffUtils.ConvertToUShortBigEndian(rowSpan); + r += deltaR; + BinaryPrimitives.WriteUInt16BigEndian(rowSpan, r); + offset += 2; + + rowSpan = rowBytes.Slice(offset, 2); + ushort deltaG = TiffUtils.ConvertToUShortBigEndian(rowSpan); + g += deltaG; + BinaryPrimitives.WriteUInt16BigEndian(rowSpan, g); + offset += 2; + + rowSpan = rowBytes.Slice(offset, 2); + ushort deltaB = TiffUtils.ConvertToUShortBigEndian(rowSpan); + b += deltaB; + BinaryPrimitives.WriteUInt16BigEndian(rowSpan, b); + offset += 2; + + rowSpan = rowBytes.Slice(offset, 2); + ushort deltaA = TiffUtils.ConvertToUShortBigEndian(rowSpan); + a += deltaA; + BinaryPrimitives.WriteUInt16BigEndian(rowSpan, a); + offset += 2; + } + } + } + else + { + for (int y = 0; y < height; y++) + { + int offset = 0; + Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + ushort r = TiffUtils.ConvertToUShortLittleEndian(rowBytes.Slice(offset, 2)); + offset += 2; + ushort g = TiffUtils.ConvertToUShortLittleEndian(rowBytes.Slice(offset, 2)); + offset += 2; + ushort b = TiffUtils.ConvertToUShortLittleEndian(rowBytes.Slice(offset, 2)); + offset += 2; + ushort a = TiffUtils.ConvertToUShortLittleEndian(rowBytes.Slice(offset, 2)); + offset += 2; + + for (int x = 1; x < width; x++) + { + Span rowSpan = rowBytes.Slice(offset, 2); + ushort deltaR = TiffUtils.ConvertToUShortLittleEndian(rowSpan); + r += deltaR; + BinaryPrimitives.WriteUInt16LittleEndian(rowSpan, r); + offset += 2; + + rowSpan = rowBytes.Slice(offset, 2); + ushort deltaG = TiffUtils.ConvertToUShortLittleEndian(rowSpan); + g += deltaG; + BinaryPrimitives.WriteUInt16LittleEndian(rowSpan, g); + offset += 2; + + rowSpan = rowBytes.Slice(offset, 2); + ushort deltaB = TiffUtils.ConvertToUShortLittleEndian(rowSpan); + b += deltaB; + BinaryPrimitives.WriteUInt16LittleEndian(rowSpan, b); + offset += 2; + + rowSpan = rowBytes.Slice(offset, 2); + ushort deltaA = TiffUtils.ConvertToUShortLittleEndian(rowSpan); + a += deltaA; + BinaryPrimitives.WriteUInt16LittleEndian(rowSpan, a); + offset += 2; + } + } + } + } + private static void UndoRgb96Bit(Span pixelBytes, int width, bool isBigEndian) { int rowBytesCount = width * 12; @@ -394,5 +522,97 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression } } } + + private static void UndoRgba128Bit(Span pixelBytes, int width, bool isBigEndian) + { + int rowBytesCount = width * 16; + int height = pixelBytes.Length / rowBytesCount; + if (isBigEndian) + { + for (int y = 0; y < height; y++) + { + int offset = 0; + Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + uint r = TiffUtils.ConvertToUIntBigEndian(rowBytes.Slice(offset, 4)); + offset += 4; + uint g = TiffUtils.ConvertToUIntBigEndian(rowBytes.Slice(offset, 4)); + offset += 4; + uint b = TiffUtils.ConvertToUIntBigEndian(rowBytes.Slice(offset, 4)); + offset += 4; + uint a = TiffUtils.ConvertToUIntBigEndian(rowBytes.Slice(offset, 4)); + offset += 4; + + for (int x = 1; x < width; x++) + { + Span rowSpan = rowBytes.Slice(offset, 4); + uint deltaR = TiffUtils.ConvertToUIntBigEndian(rowSpan); + r += deltaR; + BinaryPrimitives.WriteUInt32BigEndian(rowSpan, r); + offset += 4; + + rowSpan = rowBytes.Slice(offset, 4); + uint deltaG = TiffUtils.ConvertToUIntBigEndian(rowSpan); + g += deltaG; + BinaryPrimitives.WriteUInt32BigEndian(rowSpan, g); + offset += 4; + + rowSpan = rowBytes.Slice(offset, 4); + uint deltaB = TiffUtils.ConvertToUIntBigEndian(rowSpan); + b += deltaB; + BinaryPrimitives.WriteUInt32BigEndian(rowSpan, b); + offset += 4; + + rowSpan = rowBytes.Slice(offset, 4); + uint deltaA = TiffUtils.ConvertToUIntBigEndian(rowSpan); + a += deltaA; + BinaryPrimitives.WriteUInt32BigEndian(rowSpan, a); + offset += 4; + } + } + } + else + { + for (int y = 0; y < height; y++) + { + int offset = 0; + Span rowBytes = pixelBytes.Slice(y * rowBytesCount, rowBytesCount); + uint r = TiffUtils.ConvertToUIntLittleEndian(rowBytes.Slice(offset, 4)); + offset += 4; + uint g = TiffUtils.ConvertToUIntLittleEndian(rowBytes.Slice(offset, 4)); + offset += 4; + uint b = TiffUtils.ConvertToUIntLittleEndian(rowBytes.Slice(offset, 4)); + offset += 4; + uint a = TiffUtils.ConvertToUIntLittleEndian(rowBytes.Slice(offset, 4)); + offset += 4; + + for (int x = 1; x < width; x++) + { + Span rowSpan = rowBytes.Slice(offset, 4); + uint deltaR = TiffUtils.ConvertToUIntLittleEndian(rowSpan); + r += deltaR; + BinaryPrimitives.WriteUInt32LittleEndian(rowSpan, r); + offset += 4; + + rowSpan = rowBytes.Slice(offset, 4); + uint deltaG = TiffUtils.ConvertToUIntLittleEndian(rowSpan); + g += deltaG; + BinaryPrimitives.WriteUInt32LittleEndian(rowSpan, g); + offset += 4; + + rowSpan = rowBytes.Slice(offset, 4); + uint deltaB = TiffUtils.ConvertToUIntLittleEndian(rowSpan); + b += deltaB; + BinaryPrimitives.WriteUInt32LittleEndian(rowSpan, b); + offset += 4; + + rowSpan = rowBytes.Slice(offset, 4); + uint deltaA = TiffUtils.ConvertToUIntLittleEndian(rowSpan); + a += deltaA; + BinaryPrimitives.WriteUInt32LittleEndian(rowSpan, a); + offset += 4; + } + } + } + } } } diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffCompressorFactory.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffCompressorFactory.cs index db2b935b7..904470e81 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/TiffCompressorFactory.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/TiffCompressorFactory.cs @@ -55,6 +55,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); return new T4BitCompressor(output, allocator, width, bitsPerPixel, false); + case TiffCompression.CcittGroup4Fax: + DebugGuard.IsTrue(compressionLevel == DeflateCompressionLevel.DefaultCompression, "No deflate compression level is expected to be set"); + DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); + return new T6BitCompressor(output, allocator, width, bitsPerPixel); + case TiffCompression.Ccitt1D: DebugGuard.IsTrue(compressionLevel == DeflateCompressionLevel.DefaultCompression, "No deflate compression level is expected to be set"); DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression"); diff --git a/src/ImageSharp/Formats/Tiff/Constants/TiffCompression.cs b/src/ImageSharp/Formats/Tiff/Constants/TiffCompression.cs index 031494fc5..765f2c237 100644 --- a/src/ImageSharp/Formats/Tiff/Constants/TiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Constants/TiffCompression.cs @@ -23,11 +23,6 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Constants /// Ccitt1D = 2, - /// - /// PackBits compression - /// - PackBits = 32773, - /// /// T4-encoding: CCITT T.4 bi-level encoding (see Section 11 of the TIFF 6.0 specification). /// @@ -35,9 +30,6 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Constants /// /// T6-encoding: CCITT T.6 bi-level encoding (see Section 11 of the TIFF 6.0 specification). - /// - /// Note: The TIFF encoder does not yet support this compression and will default to use no compression instead, - /// if this is choosen. /// CcittGroup4Fax = 4, @@ -68,27 +60,48 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Constants Deflate = 8, /// - /// Deflate compression - old. + /// ITU-T Rec. T.82 coding, applying ITU-T Rec. T.85 (JBIG) (see RFC2301). /// - /// Note: The TIFF encoder does not support this compression and will default to use no compression instead, + /// Note: The TIFF encoder does not yet support this compression and will default to use no compression instead, /// if this is chosen. /// - OldDeflate = 32946, + ItuTRecT82 = 9, /// - /// ITU-T Rec. T.82 coding, applying ITU-T Rec. T.85 (JBIG) (see RFC2301). + /// ITU-T Rec. T.43 representation, using ITU-T Rec. T.82 (JBIG) (see RFC2301). /// /// Note: The TIFF encoder does not yet support this compression and will default to use no compression instead, /// if this is chosen. /// - ItuTRecT82 = 9, + ItuTRecT43 = 10, /// - /// ITU-T Rec. T.43 representation, using ITU-T Rec. T.82 (JBIG) (see RFC2301). + /// NeXT 2-bit Grey Scale compression algorithm. /// - /// Note: The TIFF encoder does not yet support this compression and will default to use no compression instead, + /// Note: The TIFF encoder does not support this compression and will default to use no compression instead, /// if this is chosen. /// - ItuTRecT43 = 10 + NeXT = 32766, + + /// + /// PackBits compression. + /// + PackBits = 32773, + + /// + /// ThunderScan 4-bit compression. + /// + /// Note: The TIFF encoder does not support this compression and will default to use no compression instead, + /// if this is chosen. + /// + ThunderScan = 32809, + + /// + /// Deflate compression - old. + /// + /// Note: The TIFF encoder does not support this compression and will default to use no compression instead, + /// if this is chosen. + /// + OldDeflate = 32946, } } diff --git a/src/ImageSharp/Formats/Tiff/ITiffDecoderOptions.cs b/src/ImageSharp/Formats/Tiff/ITiffDecoderOptions.cs index 537238439..d6d1bb8a4 100644 --- a/src/ImageSharp/Formats/Tiff/ITiffDecoderOptions.cs +++ b/src/ImageSharp/Formats/Tiff/ITiffDecoderOptions.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using SixLabors.ImageSharp.Metadata; + namespace SixLabors.ImageSharp.Formats.Tiff { /// @@ -12,5 +14,10 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// Gets a value indicating whether the metadata should be ignored when the image is being decoded. /// bool IgnoreMetadata { get; } + + /// + /// Gets the decoding mode for multi-frame images. + /// + FrameDecodingMode DecodingMode { get; } } } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs index 5d910d16e..e60562912 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs @@ -36,7 +36,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 L16 l16 = TiffUtils.L16Default; var color = default(TPixel); - color.FromVector4(TiffUtils.Vector4Default); + color.FromScaledVector4(TiffUtils.Vector4Default); int offset = 0; for (int y = top; y < top + height; y++) diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero24TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero24TiffColor{TPixel}.cs index 4cda95480..7d230dfd5 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero24TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero24TiffColor{TPixel}.cs @@ -28,7 +28,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation // Note: due to an issue with netcore 2.1 and default values and unpredictable behavior with those, // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 var color = default(TPixel); - color.FromVector4(TiffUtils.Vector4Default); + color.FromScaledVector4(TiffUtils.Vector4Default); byte[] buffer = new byte[4]; int bufferStartIdx = this.isBigEndian ? 1 : 0; diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32FloatTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32FloatTiffColor{TPixel}.cs index ee9bf8a9c..c43b121ca 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32FloatTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32FloatTiffColor{TPixel}.cs @@ -29,7 +29,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation // Note: due to an issue with netcore 2.1 and default values and unpredictable behavior with those, // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 var color = default(TPixel); - color.FromVector4(TiffUtils.Vector4Default); + color.FromScaledVector4(TiffUtils.Vector4Default); byte[] buffer = new byte[4]; int offset = 0; @@ -46,7 +46,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation offset += 4; var colorVector = new Vector4(intensity, intensity, intensity, 1.0f); - color.FromVector4(colorVector); + color.FromScaledVector4(colorVector); pixelRow[x] = color; } } @@ -59,7 +59,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation offset += 4; var colorVector = new Vector4(intensity, intensity, intensity, 1.0f); - color.FromVector4(colorVector); + color.FromScaledVector4(colorVector); pixelRow[x] = color; } } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32TiffColor{TPixel}.cs index 7367a78e3..00e4caef7 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32TiffColor{TPixel}.cs @@ -28,7 +28,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation // Note: due to an issue with netcore 2.1 and default values and unpredictable behavior with those, // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 var color = default(TPixel); - color.FromVector4(TiffUtils.Vector4Default); + color.FromScaledVector4(TiffUtils.Vector4Default); int offset = 0; for (int y = top; y < top + height; y++) diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColor{TPixel}.cs index a40fa7667..59be3f891 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColor{TPixel}.cs @@ -40,7 +40,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation int value = bitReader.ReadBits(this.bitsPerSample0); float intensity = value / this.factor; - color.FromVector4(new Vector4(intensity, intensity, intensity, 1.0f)); + color.FromScaledVector4(new Vector4(intensity, intensity, intensity, 1.0f)); pixelRow[x] = color; } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/PaletteTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/PaletteTiffColor{TPixel}.cs index 29e03c6c6..ad5793084 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/PaletteTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/PaletteTiffColor{TPixel}.cs @@ -59,7 +59,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation float r = colorMap[rOffset + i] / 65535F; float g = colorMap[gOffset + i] / 65535F; float b = colorMap[bOffset + i] / 65535F; - palette[i].FromVector4(new Vector4(r, g, b, 1.0f)); + palette[i].FromScaledVector4(new Vector4(r, g, b, 1.0f)); } return palette; diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs index a4d725bcf..609369011 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs @@ -36,7 +36,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 Rgba64 rgba = TiffUtils.Rgba64Default; var color = default(TPixel); - color.FromVector4(TiffUtils.Vector4Default); + color.FromScaledVector4(TiffUtils.Vector4Default); int offset = 0; @@ -55,7 +55,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation ulong b = TiffUtils.ConvertToUShortBigEndian(data.Slice(offset, 2)); offset += 2; - pixelRow[x] = TiffUtils.ColorFromRgba64(rgba, r, g, b, color); + pixelRow[x] = TiffUtils.ColorFromRgb64(rgba, r, g, b, color); } } else diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb16PlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb16PlanarTiffColor{TPixel}.cs index 1c61b0991..76fed3c93 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb16PlanarTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb16PlanarTiffColor{TPixel}.cs @@ -30,7 +30,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 Rgba64 rgba = TiffUtils.Rgba64Default; var color = default(TPixel); - color.FromVector4(TiffUtils.Vector4Default); + color.FromScaledVector4(TiffUtils.Vector4Default); Span redData = data[0].GetSpan(); Span greenData = data[1].GetSpan(); @@ -50,7 +50,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation offset += 2; - pixelRow[x] = TiffUtils.ColorFromRgba64(rgba, r, g, b, color); + pixelRow[x] = TiffUtils.ColorFromRgb64(rgba, r, g, b, color); } } else @@ -63,7 +63,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation offset += 2; - pixelRow[x] = TiffUtils.ColorFromRgba64(rgba, r, g, b, color); + pixelRow[x] = TiffUtils.ColorFromRgb64(rgba, r, g, b, color); } } } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb242424TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb242424TiffColor{TPixel}.cs index 985ffeb18..addf576e9 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb242424TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb242424TiffColor{TPixel}.cs @@ -28,12 +28,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation // Note: due to an issue with netcore 2.1 and default values and unpredictable behavior with those, // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 var color = default(TPixel); - color.FromVector4(TiffUtils.Vector4Default); + color.FromScaledVector4(TiffUtils.Vector4Default); int offset = 0; - byte[] buffer = new byte[4]; + Span buffer = stackalloc byte[4]; int bufferStartIdx = this.isBigEndian ? 1 : 0; - Span bufferSpan = buffer.AsSpan(bufferStartIdx); + Span bufferSpan = buffer.Slice(bufferStartIdx); for (int y = top; y < top + height; y++) { Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb24PlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb24PlanarTiffColor{TPixel}.cs index ac4435db6..2eda3b5af 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb24PlanarTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb24PlanarTiffColor{TPixel}.cs @@ -29,14 +29,14 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation // Note: due to an issue with netcore 2.1 and default values and unpredictable behavior with those, // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 var color = default(TPixel); - color.FromVector4(TiffUtils.Vector4Default); - byte[] buffer = new byte[4]; + color.FromScaledVector4(TiffUtils.Vector4Default); + Span buffer = stackalloc byte[4]; int bufferStartIdx = this.isBigEndian ? 1 : 0; Span redData = data[0].GetSpan(); Span greenData = data[1].GetSpan(); Span blueData = data[2].GetSpan(); - Span bufferSpan = buffer.AsSpan(bufferStartIdx); + Span bufferSpan = buffer.Slice(bufferStartIdx); int offset = 0; for (int y = top; y < top + height; y++) diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb323232TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb323232TiffColor{TPixel}.cs index bf1e65e1c..02319bfa6 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb323232TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb323232TiffColor{TPixel}.cs @@ -28,7 +28,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation // Note: due to an issue with netcore 2.1 and default values and unpredictable behavior with those, // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 var color = default(TPixel); - color.FromVector4(TiffUtils.Vector4Default); + color.FromScaledVector4(TiffUtils.Vector4Default); int offset = 0; for (int y = top; y < top + height; y++) diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb32PlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb32PlanarTiffColor{TPixel}.cs index cdc6942bd..26f75bfcf 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb32PlanarTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb32PlanarTiffColor{TPixel}.cs @@ -29,7 +29,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation // Note: due to an issue with netcore 2.1 and default values and unpredictable behavior with those, // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 var color = default(TPixel); - color.FromVector4(TiffUtils.Vector4Default); + color.FromScaledVector4(TiffUtils.Vector4Default); Span redData = data[0].GetSpan(); Span greenData = data[1].GetSpan(); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbFloat323232TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbFloat323232TiffColor{TPixel}.cs index 4dc3295a4..7fd98dd50 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbFloat323232TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbFloat323232TiffColor{TPixel}.cs @@ -29,7 +29,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation // Note: due to an issue with netcore 2.1 and default values and unpredictable behavior with those, // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 var color = default(TPixel); - color.FromVector4(TiffUtils.Vector4Default); + color.FromScaledVector4(TiffUtils.Vector4Default); int offset = 0; byte[] buffer = new byte[4]; @@ -57,7 +57,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation offset += 4; var colorVector = new Vector4(r, g, b, 1.0f); - color.FromVector4(colorVector); + color.FromScaledVector4(colorVector); pixelRow[x] = color; } } @@ -78,7 +78,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation offset += 4; var colorVector = new Vector4(r, g, b, 1.0f); - color.FromVector4(colorVector); + color.FromScaledVector4(colorVector); pixelRow[x] = color; } } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor{TPixel}.cs index 54466e05b..f1ff91382 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor{TPixel}.cs @@ -65,7 +65,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation float g = gBitReader.ReadBits(this.bitsPerSampleG) / this.gFactor; float b = bBitReader.ReadBits(this.bitsPerSampleB) / this.bFactor; - color.FromVector4(new Vector4(r, g, b, 1.0f)); + color.FromScaledVector4(new Vector4(r, g, b, 1.0f)); pixelRow[x] = color; } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbTiffColor{TPixel}.cs index 4a887c426..9d037cec7 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbTiffColor{TPixel}.cs @@ -54,7 +54,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation float g = bitReader.ReadBits(this.bitsPerSampleG) / this.gFactor; float b = bitReader.ReadBits(this.bitsPerSampleB) / this.bFactor; - color.FromVector4(new Vector4(r, g, b, 1.0f)); + color.FromScaledVector4(new Vector4(r, g, b, 1.0f)); pixelRow[x] = color; } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba16161616TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba16161616TiffColor{TPixel}.cs new file mode 100644 index 000000000..0340438cb --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba16161616TiffColor{TPixel}.cs @@ -0,0 +1,98 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Numerics; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements the 'RGB' photometric interpretation with an alpha channel and with 16 bits for each channel. + /// + internal class Rgba16161616TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + private readonly Configuration configuration; + + private readonly MemoryAllocator memoryAllocator; + + private readonly TiffExtraSampleType? extraSamplesType; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration. + /// The memory allocator. + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + /// The type of the extra samples. + public Rgba16161616TiffColor(Configuration configuration, MemoryAllocator memoryAllocator, TiffExtraSampleType? extraSamplesType, bool isBigEndian) + { + this.configuration = configuration; + this.isBigEndian = isBigEndian; + this.memoryAllocator = memoryAllocator; + this.extraSamplesType = extraSamplesType; + } + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + // Note: due to an issue with netcore 2.1 and default values and unpredictable behavior with those, + // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 + Rgba64 rgba = TiffUtils.Rgba64Default; + var color = default(TPixel); + color.FromScaledVector4(TiffUtils.Vector4Default); + + bool hasAssociatedAlpha = this.extraSamplesType.HasValue && this.extraSamplesType == TiffExtraSampleType.AssociatedAlphaData; + int offset = 0; + + using IMemoryOwner vectors = hasAssociatedAlpha ? this.memoryAllocator.Allocate(width) : null; + Span vectorsSpan = hasAssociatedAlpha ? vectors.GetSpan() : Span.Empty; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + ulong r = TiffUtils.ConvertToUShortBigEndian(data.Slice(offset, 2)); + offset += 2; + ulong g = TiffUtils.ConvertToUShortBigEndian(data.Slice(offset, 2)); + offset += 2; + ulong b = TiffUtils.ConvertToUShortBigEndian(data.Slice(offset, 2)); + offset += 2; + ulong a = TiffUtils.ConvertToUShortBigEndian(data.Slice(offset, 2)); + offset += 2; + + pixelRow[x] = hasAssociatedAlpha ? + TiffUtils.ColorFromRgba64Premultiplied(rgba, r, g, b, a, color) : + TiffUtils.ColorFromRgba64(rgba, r, g, b, a, color); + } + } + else + { + int byteCount = pixelRow.Length * 8; + PixelOperations.Instance.FromRgba64Bytes( + this.configuration, + data.Slice(offset, byteCount), + pixelRow, + pixelRow.Length); + + if (hasAssociatedAlpha) + { + PixelOperations.Instance.ToVector4(this.configuration, pixelRow, vectorsSpan); + PixelOperations.Instance.FromVector4Destructive(this.configuration, vectorsSpan, pixelRow, PixelConversionModifiers.Premultiply | PixelConversionModifiers.Scale); + } + + offset += byteCount; + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba16PlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba16PlanarTiffColor{TPixel}.cs new file mode 100644 index 000000000..856d810d3 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba16PlanarTiffColor{TPixel}.cs @@ -0,0 +1,87 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements the 'RGB' photometric interpretation with an alpha channel and with 'Planar' layout for each color channel with 16 bit. + /// + internal class Rgba16PlanarTiffColor : TiffBasePlanarColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + private readonly TiffExtraSampleType? extraSamplesType; + + /// + /// Initializes a new instance of the class. + /// + /// The extra samples type. + /// If set to true decodes the pixel data as big endian, otherwise as little endian. + public Rgba16PlanarTiffColor(TiffExtraSampleType? extraSamplesType, bool isBigEndian) + { + this.extraSamplesType = extraSamplesType; + this.isBigEndian = isBigEndian; + } + + /// + public override void Decode(IMemoryOwner[] data, Buffer2D pixels, int left, int top, int width, int height) + { + // Note: due to an issue with netcore 2.1 and default values and unpredictable behavior with those, + // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 + Rgba64 rgba = TiffUtils.Rgba64Default; + var color = default(TPixel); + color.FromScaledVector4(TiffUtils.Vector4Default); + + Span redData = data[0].GetSpan(); + Span greenData = data[1].GetSpan(); + Span blueData = data[2].GetSpan(); + Span alphaData = data[3].GetSpan(); + + bool hasAssociatedAlpha = this.extraSamplesType.HasValue && this.extraSamplesType == TiffExtraSampleType.AssociatedAlphaData; + int offset = 0; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + ulong r = TiffUtils.ConvertToUShortBigEndian(redData.Slice(offset, 2)); + ulong g = TiffUtils.ConvertToUShortBigEndian(greenData.Slice(offset, 2)); + ulong b = TiffUtils.ConvertToUShortBigEndian(blueData.Slice(offset, 2)); + ulong a = TiffUtils.ConvertToUShortBigEndian(alphaData.Slice(offset, 2)); + + offset += 2; + + pixelRow[x] = hasAssociatedAlpha ? + TiffUtils.ColorFromRgba64Premultiplied(rgba, r, g, b, a, color) : + TiffUtils.ColorFromRgba64(rgba, r, g, b, a, color); + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + ulong r = TiffUtils.ConvertToUShortLittleEndian(redData.Slice(offset, 2)); + ulong g = TiffUtils.ConvertToUShortLittleEndian(greenData.Slice(offset, 2)); + ulong b = TiffUtils.ConvertToUShortLittleEndian(blueData.Slice(offset, 2)); + ulong a = TiffUtils.ConvertToUShortLittleEndian(alphaData.Slice(offset, 2)); + + offset += 2; + + pixelRow[x] = hasAssociatedAlpha ? + TiffUtils.ColorFromRgba64Premultiplied(rgba, r, g, b, a, color) : + TiffUtils.ColorFromRgba64(rgba, r, g, b, a, color); + } + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba24242424TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba24242424TiffColor{TPixel}.cs new file mode 100644 index 000000000..2ce30252f --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba24242424TiffColor{TPixel}.cs @@ -0,0 +1,104 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements the 'RGB' photometric interpretation with an alpha channel and with 24 bits for each channel. + /// + internal class Rgba24242424TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + private readonly TiffExtraSampleType? extraSamplesType; + + /// + /// Initializes a new instance of the class. + /// + /// The type of the extra samples. + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public Rgba24242424TiffColor(TiffExtraSampleType? extraSamplesType, bool isBigEndian) + { + this.extraSamplesType = extraSamplesType; + this.isBigEndian = isBigEndian; + } + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + // Note: due to an issue with netcore 2.1 and default values and unpredictable behavior with those, + // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 + var color = default(TPixel); + color.FromScaledVector4(TiffUtils.Vector4Default); + + bool hasAssociatedAlpha = this.extraSamplesType.HasValue && this.extraSamplesType == TiffExtraSampleType.AssociatedAlphaData; + int offset = 0; + + Span buffer = stackalloc byte[4]; + int bufferStartIdx = this.isBigEndian ? 1 : 0; + + Span bufferSpan = buffer.Slice(bufferStartIdx); + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + data.Slice(offset, 3).CopyTo(bufferSpan); + ulong r = TiffUtils.ConvertToUIntBigEndian(buffer); + offset += 3; + + data.Slice(offset, 3).CopyTo(bufferSpan); + ulong g = TiffUtils.ConvertToUIntBigEndian(buffer); + offset += 3; + + data.Slice(offset, 3).CopyTo(bufferSpan); + ulong b = TiffUtils.ConvertToUIntBigEndian(buffer); + offset += 3; + + data.Slice(offset, 3).CopyTo(bufferSpan); + ulong a = TiffUtils.ConvertToUIntBigEndian(buffer); + offset += 3; + + pixelRow[x] = hasAssociatedAlpha ? + TiffUtils.ColorScaleTo24BitPremultiplied(r, g, b, a, color) : + TiffUtils.ColorScaleTo24Bit(r, g, b, a, color); + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + data.Slice(offset, 3).CopyTo(bufferSpan); + ulong r = TiffUtils.ConvertToUIntLittleEndian(buffer); + offset += 3; + + data.Slice(offset, 3).CopyTo(bufferSpan); + ulong g = TiffUtils.ConvertToUIntLittleEndian(buffer); + offset += 3; + + data.Slice(offset, 3).CopyTo(bufferSpan); + ulong b = TiffUtils.ConvertToUIntLittleEndian(buffer); + offset += 3; + + data.Slice(offset, 3).CopyTo(bufferSpan); + ulong a = TiffUtils.ConvertToUIntLittleEndian(buffer); + offset += 3; + + pixelRow[x] = hasAssociatedAlpha ? + TiffUtils.ColorScaleTo24BitPremultiplied(r, g, b, a, color) : + TiffUtils.ColorScaleTo24Bit(r, g, b, a, color); + } + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba24PlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba24PlanarTiffColor{TPixel}.cs new file mode 100644 index 000000000..89172cfe7 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba24PlanarTiffColor{TPixel}.cs @@ -0,0 +1,97 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements the 'RGB' photometric interpretation with an alpha channel and with 'Planar' layout for each color channel with 24 bit. + /// + internal class Rgba24PlanarTiffColor : TiffBasePlanarColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + private readonly TiffExtraSampleType? extraSamplesType; + + /// + /// Initializes a new instance of the class. + /// + /// The extra samples type. + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public Rgba24PlanarTiffColor(TiffExtraSampleType? extraSamplesType, bool isBigEndian) + { + this.extraSamplesType = extraSamplesType; + this.isBigEndian = isBigEndian; + } + + /// + public override void Decode(IMemoryOwner[] data, Buffer2D pixels, int left, int top, int width, int height) + { + // Note: due to an issue with netcore 2.1 and default values and unpredictable behavior with those, + // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 + var color = default(TPixel); + color.FromScaledVector4(TiffUtils.Vector4Default); + Span buffer = stackalloc byte[4]; + int bufferStartIdx = this.isBigEndian ? 1 : 0; + + Span redData = data[0].GetSpan(); + Span greenData = data[1].GetSpan(); + Span blueData = data[2].GetSpan(); + Span alphaData = data[3].GetSpan(); + Span bufferSpan = buffer.Slice(bufferStartIdx); + + bool hasAssociatedAlpha = this.extraSamplesType.HasValue && this.extraSamplesType == TiffExtraSampleType.AssociatedAlphaData; + int offset = 0; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + redData.Slice(offset, 3).CopyTo(bufferSpan); + ulong r = TiffUtils.ConvertToUIntBigEndian(buffer); + greenData.Slice(offset, 3).CopyTo(bufferSpan); + ulong g = TiffUtils.ConvertToUIntBigEndian(buffer); + blueData.Slice(offset, 3).CopyTo(bufferSpan); + ulong b = TiffUtils.ConvertToUIntBigEndian(buffer); + alphaData.Slice(offset, 3).CopyTo(bufferSpan); + ulong a = TiffUtils.ConvertToUIntBigEndian(buffer); + + offset += 3; + + pixelRow[x] = hasAssociatedAlpha ? + TiffUtils.ColorScaleTo24BitPremultiplied(r, g, b, a, color) : + TiffUtils.ColorScaleTo24Bit(r, g, b, a, color); + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + redData.Slice(offset, 3).CopyTo(bufferSpan); + ulong r = TiffUtils.ConvertToUIntLittleEndian(buffer); + greenData.Slice(offset, 3).CopyTo(bufferSpan); + ulong g = TiffUtils.ConvertToUIntLittleEndian(buffer); + blueData.Slice(offset, 3).CopyTo(bufferSpan); + ulong b = TiffUtils.ConvertToUIntLittleEndian(buffer); + alphaData.Slice(offset, 3).CopyTo(bufferSpan); + ulong a = TiffUtils.ConvertToUIntLittleEndian(buffer); + + offset += 3; + + pixelRow[x] = hasAssociatedAlpha ? + TiffUtils.ColorScaleTo24BitPremultiplied(r, g, b, a, color) : + TiffUtils.ColorScaleTo24Bit(r, g, b, a, color); + } + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba32323232TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba32323232TiffColor{TPixel}.cs new file mode 100644 index 000000000..8ee9eb0bf --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba32323232TiffColor{TPixel}.cs @@ -0,0 +1,92 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements the 'RGB' photometric interpretation with an alpha channel and with 32 bits for each channel. + /// + internal class Rgba32323232TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + private readonly TiffExtraSampleType? extraSamplesType; + + /// + /// Initializes a new instance of the class. + /// + /// The type of the extra samples. + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public Rgba32323232TiffColor(TiffExtraSampleType? extraSamplesType, bool isBigEndian) + { + this.extraSamplesType = extraSamplesType; + this.isBigEndian = isBigEndian; + } + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + // Note: due to an issue with netcore 2.1 and default values and unpredictable behavior with those, + // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 + var color = default(TPixel); + color.FromScaledVector4(TiffUtils.Vector4Default); + + bool hasAssociatedAlpha = this.extraSamplesType.HasValue && this.extraSamplesType == TiffExtraSampleType.AssociatedAlphaData; + int offset = 0; + + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + ulong r = TiffUtils.ConvertToUIntBigEndian(data.Slice(offset, 4)); + offset += 4; + + ulong g = TiffUtils.ConvertToUIntBigEndian(data.Slice(offset, 4)); + offset += 4; + + ulong b = TiffUtils.ConvertToUIntBigEndian(data.Slice(offset, 4)); + offset += 4; + + ulong a = TiffUtils.ConvertToUIntBigEndian(data.Slice(offset, 4)); + offset += 4; + + pixelRow[x] = hasAssociatedAlpha ? + TiffUtils.ColorScaleTo32BitPremultiplied(r, g, b, a, color) : + TiffUtils.ColorScaleTo32Bit(r, g, b, a, color); + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + ulong r = TiffUtils.ConvertToUIntLittleEndian(data.Slice(offset, 4)); + offset += 4; + + ulong g = TiffUtils.ConvertToUIntLittleEndian(data.Slice(offset, 4)); + offset += 4; + + ulong b = TiffUtils.ConvertToUIntLittleEndian(data.Slice(offset, 4)); + offset += 4; + + ulong a = TiffUtils.ConvertToUIntLittleEndian(data.Slice(offset, 4)); + offset += 4; + + pixelRow[x] = hasAssociatedAlpha ? + TiffUtils.ColorScaleTo32BitPremultiplied(r, g, b, a, color) : + TiffUtils.ColorScaleTo32Bit(r, g, b, a, color); + } + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba32PlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba32PlanarTiffColor{TPixel}.cs new file mode 100644 index 000000000..c98ac1cf0 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba32PlanarTiffColor{TPixel}.cs @@ -0,0 +1,86 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements the 'RGB' photometric interpretation with an alpha channel and a 'Planar' layout for each color channel with 32 bit. + /// + internal class Rgba32PlanarTiffColor : TiffBasePlanarColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + private readonly TiffExtraSampleType? extraSamplesType; + + /// + /// Initializes a new instance of the class. + /// + /// The extra samples type. + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public Rgba32PlanarTiffColor(TiffExtraSampleType? extraSamplesType, bool isBigEndian) + { + this.extraSamplesType = extraSamplesType; + this.isBigEndian = isBigEndian; + } + + /// + public override void Decode(IMemoryOwner[] data, Buffer2D pixels, int left, int top, int width, int height) + { + // Note: due to an issue with netcore 2.1 and default values and unpredictable behavior with those, + // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 + var color = default(TPixel); + color.FromScaledVector4(TiffUtils.Vector4Default); + + Span redData = data[0].GetSpan(); + Span greenData = data[1].GetSpan(); + Span blueData = data[2].GetSpan(); + Span alphaData = data[3].GetSpan(); + + bool hasAssociatedAlpha = this.extraSamplesType.HasValue && this.extraSamplesType == TiffExtraSampleType.AssociatedAlphaData; + int offset = 0; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + ulong r = TiffUtils.ConvertToUIntBigEndian(redData.Slice(offset, 4)); + ulong g = TiffUtils.ConvertToUIntBigEndian(greenData.Slice(offset, 4)); + ulong b = TiffUtils.ConvertToUIntBigEndian(blueData.Slice(offset, 4)); + ulong a = TiffUtils.ConvertToUIntBigEndian(alphaData.Slice(offset, 4)); + + offset += 4; + + pixelRow[x] = hasAssociatedAlpha ? + TiffUtils.ColorScaleTo32BitPremultiplied(r, g, b, a, color) : + TiffUtils.ColorScaleTo32Bit(r, g, b, a, color); + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + ulong r = TiffUtils.ConvertToUIntLittleEndian(redData.Slice(offset, 4)); + ulong g = TiffUtils.ConvertToUIntLittleEndian(greenData.Slice(offset, 4)); + ulong b = TiffUtils.ConvertToUIntLittleEndian(blueData.Slice(offset, 4)); + ulong a = TiffUtils.ConvertToUIntLittleEndian(alphaData.Slice(offset, 4)); + + offset += 4; + + pixelRow[x] = hasAssociatedAlpha ? + TiffUtils.ColorScaleTo32BitPremultiplied(r, g, b, a, color) : + TiffUtils.ColorScaleTo32Bit(r, g, b, a, color); + } + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba8888TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba8888TiffColor{TPixel}.cs new file mode 100644 index 000000000..967a68ad0 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba8888TiffColor{TPixel}.cs @@ -0,0 +1,62 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Numerics; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements the 'RGB' photometric interpretation with an alpha channel and 8 bits per channel. + /// + internal class Rgba8888TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly Configuration configuration; + + private readonly MemoryAllocator memoryAllocator; + + private readonly TiffExtraSampleType? extraSamplesType; + + public Rgba8888TiffColor(Configuration configuration, MemoryAllocator memoryAllocator, TiffExtraSampleType? extraSamplesType) + { + this.configuration = configuration; + this.memoryAllocator = memoryAllocator; + this.extraSamplesType = extraSamplesType; + } + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + int offset = 0; + bool hasAssociatedAlpha = this.extraSamplesType.HasValue && this.extraSamplesType == TiffExtraSampleType.AssociatedAlphaData; + + var color = default(TPixel); + color.FromScaledVector4(TiffUtils.Vector4Default); + using IMemoryOwner vectors = hasAssociatedAlpha ? this.memoryAllocator.Allocate(width) : null; + Span vectorsSpan = hasAssociatedAlpha ? vectors.GetSpan() : Span.Empty; + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + int byteCount = pixelRow.Length * 4; + PixelOperations.Instance.FromRgba32Bytes( + this.configuration, + data.Slice(offset, byteCount), + pixelRow, + pixelRow.Length); + + if (hasAssociatedAlpha) + { + PixelOperations.Instance.ToVector4(this.configuration, pixelRow, vectorsSpan); + PixelOperations.Instance.FromVector4Destructive(this.configuration, vectorsSpan, pixelRow, PixelConversionModifiers.Premultiply | PixelConversionModifiers.Scale); + } + + offset += byteCount; + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbaFloat32323232TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbaFloat32323232TiffColor{TPixel}.cs new file mode 100644 index 000000000..f95045ec5 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbaFloat32323232TiffColor{TPixel}.cs @@ -0,0 +1,97 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements the 'RGB' photometric interpretation with an alpha channel and with 32 bits for each channel. + /// + internal class RgbaFloat32323232TiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly bool isBigEndian; + + /// + /// Initializes a new instance of the class. + /// + /// if set to true decodes the pixel data as big endian, otherwise as little endian. + public RgbaFloat32323232TiffColor(bool isBigEndian) => this.isBigEndian = isBigEndian; + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + // Note: due to an issue with netcore 2.1 and default values and unpredictable behavior with those, + // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 + var color = default(TPixel); + color.FromScaledVector4(TiffUtils.Vector4Default); + int offset = 0; + byte[] buffer = new byte[4]; + + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + + if (this.isBigEndian) + { + for (int x = 0; x < pixelRow.Length; x++) + { + data.Slice(offset, 4).CopyTo(buffer); + Array.Reverse(buffer); + float r = BitConverter.ToSingle(buffer, 0); + offset += 4; + + data.Slice(offset, 4).CopyTo(buffer); + Array.Reverse(buffer); + float g = BitConverter.ToSingle(buffer, 0); + offset += 4; + + data.Slice(offset, 4).CopyTo(buffer); + Array.Reverse(buffer); + float b = BitConverter.ToSingle(buffer, 0); + offset += 4; + + data.Slice(offset, 4).CopyTo(buffer); + Array.Reverse(buffer); + float a = BitConverter.ToSingle(buffer, 0); + offset += 4; + + var colorVector = new Vector4(r, g, b, a); + color.FromScaledVector4(colorVector); + pixelRow[x] = color; + } + } + else + { + for (int x = 0; x < pixelRow.Length; x++) + { + data.Slice(offset, 4).CopyTo(buffer); + float r = BitConverter.ToSingle(buffer, 0); + offset += 4; + + data.Slice(offset, 4).CopyTo(buffer); + float g = BitConverter.ToSingle(buffer, 0); + offset += 4; + + data.Slice(offset, 4).CopyTo(buffer); + float b = BitConverter.ToSingle(buffer, 0); + offset += 4; + + data.Slice(offset, 4).CopyTo(buffer); + float a = BitConverter.ToSingle(buffer, 0); + offset += 4; + + var colorVector = new Vector4(r, g, b, a); + color.FromScaledVector4(colorVector); + pixelRow[x] = color; + } + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbaPlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbaPlanarTiffColor{TPixel}.cs new file mode 100644 index 000000000..e2dbdfb00 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbaPlanarTiffColor{TPixel}.cs @@ -0,0 +1,101 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Numerics; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements the 'RGB' photometric interpretation with an alpha channel and with 'Planar' layout (for all bit depths). + /// + internal class RgbaPlanarTiffColor : TiffBasePlanarColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly float rFactor; + + private readonly float gFactor; + + private readonly float bFactor; + + private readonly float aFactor; + + private readonly ushort bitsPerSampleR; + + private readonly ushort bitsPerSampleG; + + private readonly ushort bitsPerSampleB; + + private readonly ushort bitsPerSampleA; + + private readonly TiffExtraSampleType? extraSampleType; + + public RgbaPlanarTiffColor(TiffExtraSampleType? extraSampleType, TiffBitsPerSample bitsPerSample) + { + this.bitsPerSampleR = bitsPerSample.Channel0; + this.bitsPerSampleG = bitsPerSample.Channel1; + this.bitsPerSampleB = bitsPerSample.Channel2; + this.bitsPerSampleA = bitsPerSample.Channel3; + + this.rFactor = (1 << this.bitsPerSampleR) - 1.0f; + this.gFactor = (1 << this.bitsPerSampleG) - 1.0f; + this.bFactor = (1 << this.bitsPerSampleB) - 1.0f; + this.aFactor = (1 << this.bitsPerSampleA) - 1.0f; + + this.extraSampleType = extraSampleType; + } + + /// + /// Decodes pixel data using the current photometric interpretation. + /// + /// The buffers to read image data from. + /// The image buffer to write pixels to. + /// The x-coordinate of the left-hand side of the image block. + /// The y-coordinate of the top of the image block. + /// The width of the image block. + /// The height of the image block. + public override void Decode(IMemoryOwner[] data, Buffer2D pixels, int left, int top, int width, int height) + { + var color = default(TPixel); + bool hasAssociatedAlpha = this.extraSampleType.HasValue && this.extraSampleType == TiffExtraSampleType.AssociatedAlphaData; + + var rBitReader = new BitReader(data[0].GetSpan()); + var gBitReader = new BitReader(data[1].GetSpan()); + var bBitReader = new BitReader(data[2].GetSpan()); + var aBitReader = new BitReader(data[3].GetSpan()); + + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + for (int x = 0; x < pixelRow.Length; x++) + { + float r = rBitReader.ReadBits(this.bitsPerSampleR) / this.rFactor; + float g = gBitReader.ReadBits(this.bitsPerSampleG) / this.gFactor; + float b = bBitReader.ReadBits(this.bitsPerSampleB) / this.bFactor; + float a = aBitReader.ReadBits(this.bitsPerSampleA) / this.aFactor; + + var vec = new Vector4(r, g, b, a); + if (hasAssociatedAlpha) + { + color = TiffUtils.UnPremultiply(ref vec, color); + } + else + { + color.FromScaledVector4(vec); + } + + pixelRow[x] = color; + } + + rBitReader.NextRow(); + gBitReader.NextRow(); + bBitReader.NextRow(); + aBitReader.NextRow(); + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbaTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbaTiffColor{TPixel}.cs new file mode 100644 index 000000000..74b816dbc --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbaTiffColor{TPixel}.cs @@ -0,0 +1,86 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.Formats.Tiff.Utils; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation +{ + /// + /// Implements the 'RGB' photometric interpretation with alpha channel (for all bit depths). + /// + internal class RgbaTiffColor : TiffBaseColorDecoder + where TPixel : unmanaged, IPixel + { + private readonly float rFactor; + + private readonly float gFactor; + + private readonly float bFactor; + + private readonly float aFactor; + + private readonly ushort bitsPerSampleR; + + private readonly ushort bitsPerSampleG; + + private readonly ushort bitsPerSampleB; + + private readonly ushort bitsPerSampleA; + + private readonly TiffExtraSampleType? extraSamplesType; + + public RgbaTiffColor(TiffExtraSampleType? extraSampleType, TiffBitsPerSample bitsPerSample) + { + this.bitsPerSampleR = bitsPerSample.Channel0; + this.bitsPerSampleG = bitsPerSample.Channel1; + this.bitsPerSampleB = bitsPerSample.Channel2; + this.bitsPerSampleA = bitsPerSample.Channel3; + + this.rFactor = (1 << this.bitsPerSampleR) - 1.0f; + this.gFactor = (1 << this.bitsPerSampleG) - 1.0f; + this.bFactor = (1 << this.bitsPerSampleB) - 1.0f; + this.aFactor = (1 << this.bitsPerSampleA) - 1.0f; + + this.extraSamplesType = extraSampleType; + } + + /// + public override void Decode(ReadOnlySpan data, Buffer2D pixels, int left, int top, int width, int height) + { + var color = default(TPixel); + + var bitReader = new BitReader(data); + + bool hasAssociatedAlpha = this.extraSamplesType.HasValue && this.extraSamplesType == TiffExtraSampleType.AssociatedAlphaData; + + for (int y = top; y < top + height; y++) + { + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); + for (int x = 0; x < pixelRow.Length; x++) + { + float r = bitReader.ReadBits(this.bitsPerSampleR) / this.rFactor; + float g = bitReader.ReadBits(this.bitsPerSampleG) / this.gFactor; + float b = bitReader.ReadBits(this.bitsPerSampleB) / this.bFactor; + float a = bitReader.ReadBits(this.bitsPerSampleB) / this.aFactor; + + var vec = new Vector4(r, g, b, a); + if (hasAssociatedAlpha) + { + pixelRow[x] = TiffUtils.UnPremultiply(ref vec, color); + } + else + { + color.FromScaledVector4(vec); + pixelRow[x] = color; + } + } + + bitReader.NextRow(); + } + } + } +} diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs index a8c4cef4e..c95d03946 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorDecoderFactory{TPixel}.cs @@ -14,6 +14,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation MemoryAllocator memoryAllocator, TiffColorType colorType, TiffBitsPerSample bitsPerSample, + TiffExtraSampleType? extraSampleType, ushort[] colorMap, Rational[] referenceBlackAndWhite, Rational[] ycbcrCoefficients, @@ -116,6 +117,38 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation DebugGuard.IsTrue(colorMap == null, "colorMap"); return new RgbTiffColor(bitsPerSample); + case TiffColorType.Rgba2222: + DebugGuard.IsTrue( + bitsPerSample.Channels == 4 + && bitsPerSample.Channel3 == 2 + && bitsPerSample.Channel2 == 2 + && bitsPerSample.Channel1 == 2 + && bitsPerSample.Channel0 == 2, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new RgbaTiffColor(extraSampleType, bitsPerSample); + + case TiffColorType.Rgb333: + DebugGuard.IsTrue( + bitsPerSample.Channels == 3 + && bitsPerSample.Channel2 == 3 + && bitsPerSample.Channel1 == 3 + && bitsPerSample.Channel0 == 3, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new RgbTiffColor(bitsPerSample); + + case TiffColorType.Rgba3333: + DebugGuard.IsTrue( + bitsPerSample.Channels == 4 + && bitsPerSample.Channel3 == 3 + && bitsPerSample.Channel2 == 3 + && bitsPerSample.Channel1 == 3 + && bitsPerSample.Channel0 == 3, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new RgbaTiffColor(extraSampleType, bitsPerSample); + case TiffColorType.Rgb444: DebugGuard.IsTrue( bitsPerSample.Channels == 3 @@ -126,6 +159,59 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation DebugGuard.IsTrue(colorMap == null, "colorMap"); return new Rgb444TiffColor(); + case TiffColorType.Rgba4444: + DebugGuard.IsTrue( + bitsPerSample.Channels == 4 + && bitsPerSample.Channel3 == 4 + && bitsPerSample.Channel2 == 4 + && bitsPerSample.Channel1 == 4 + && bitsPerSample.Channel0 == 4, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new RgbaTiffColor(extraSampleType, bitsPerSample); + + case TiffColorType.Rgb555: + DebugGuard.IsTrue( + bitsPerSample.Channels == 3 + && bitsPerSample.Channel2 == 5 + && bitsPerSample.Channel1 == 5 + && bitsPerSample.Channel0 == 5, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new RgbTiffColor(bitsPerSample); + + case TiffColorType.Rgba5555: + DebugGuard.IsTrue( + bitsPerSample.Channels == 4 + && bitsPerSample.Channel3 == 5 + && bitsPerSample.Channel2 == 5 + && bitsPerSample.Channel1 == 5 + && bitsPerSample.Channel0 == 5, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new RgbaTiffColor(extraSampleType, bitsPerSample); + + case TiffColorType.Rgb666: + DebugGuard.IsTrue( + bitsPerSample.Channels == 3 + && bitsPerSample.Channel2 == 6 + && bitsPerSample.Channel1 == 6 + && bitsPerSample.Channel0 == 6, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new RgbTiffColor(bitsPerSample); + + case TiffColorType.Rgba6666: + DebugGuard.IsTrue( + bitsPerSample.Channels == 4 + && bitsPerSample.Channel3 == 6 + && bitsPerSample.Channel2 == 6 + && bitsPerSample.Channel1 == 6 + && bitsPerSample.Channel0 == 6, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new RgbaTiffColor(extraSampleType, bitsPerSample); + case TiffColorType.Rgb888: DebugGuard.IsTrue( bitsPerSample.Channels == 3 @@ -136,6 +222,17 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation DebugGuard.IsTrue(colorMap == null, "colorMap"); return new Rgb888TiffColor(configuration); + case TiffColorType.Rgba8888: + DebugGuard.IsTrue( + bitsPerSample.Channels == 4 + && bitsPerSample.Channel3 == 8 + && bitsPerSample.Channel2 == 8 + && bitsPerSample.Channel1 == 8 + && bitsPerSample.Channel0 == 8, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new Rgba8888TiffColor(configuration, memoryAllocator, extraSampleType); + case TiffColorType.Rgb101010: DebugGuard.IsTrue( bitsPerSample.Channels == 3 @@ -146,6 +243,17 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation DebugGuard.IsTrue(colorMap == null, "colorMap"); return new RgbTiffColor(bitsPerSample); + case TiffColorType.Rgba10101010: + DebugGuard.IsTrue( + bitsPerSample.Channels == 4 + && bitsPerSample.Channel3 == 10 + && bitsPerSample.Channel2 == 10 + && bitsPerSample.Channel1 == 10 + && bitsPerSample.Channel0 == 10, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new RgbaTiffColor(extraSampleType, bitsPerSample); + case TiffColorType.Rgb121212: DebugGuard.IsTrue( bitsPerSample.Channels == 3 @@ -156,6 +264,17 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation DebugGuard.IsTrue(colorMap == null, "colorMap"); return new RgbTiffColor(bitsPerSample); + case TiffColorType.Rgba12121212: + DebugGuard.IsTrue( + bitsPerSample.Channels == 4 + && bitsPerSample.Channel3 == 12 + && bitsPerSample.Channel2 == 12 + && bitsPerSample.Channel1 == 12 + && bitsPerSample.Channel0 == 12, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new RgbaTiffColor(extraSampleType, bitsPerSample); + case TiffColorType.Rgb141414: DebugGuard.IsTrue( bitsPerSample.Channels == 3 @@ -166,6 +285,17 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation DebugGuard.IsTrue(colorMap == null, "colorMap"); return new RgbTiffColor(bitsPerSample); + case TiffColorType.Rgba14141414: + DebugGuard.IsTrue( + bitsPerSample.Channels == 4 + && bitsPerSample.Channel3 == 14 + && bitsPerSample.Channel2 == 14 + && bitsPerSample.Channel1 == 14 + && bitsPerSample.Channel0 == 14, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new RgbaTiffColor(extraSampleType, bitsPerSample); + case TiffColorType.Rgb161616: DebugGuard.IsTrue( bitsPerSample.Channels == 3 @@ -176,6 +306,17 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation DebugGuard.IsTrue(colorMap == null, "colorMap"); return new Rgb161616TiffColor(configuration, isBigEndian: byteOrder == ByteOrder.BigEndian); + case TiffColorType.Rgba16161616: + DebugGuard.IsTrue( + bitsPerSample.Channels == 4 + && bitsPerSample.Channel3 == 16 + && bitsPerSample.Channel2 == 16 + && bitsPerSample.Channel1 == 16 + && bitsPerSample.Channel0 == 16, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new Rgba16161616TiffColor(configuration, memoryAllocator, extraSampleType, isBigEndian: byteOrder == ByteOrder.BigEndian); + case TiffColorType.Rgb242424: DebugGuard.IsTrue( bitsPerSample.Channels == 3 @@ -186,6 +327,17 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation DebugGuard.IsTrue(colorMap == null, "colorMap"); return new Rgb242424TiffColor(isBigEndian: byteOrder == ByteOrder.BigEndian); + case TiffColorType.Rgba24242424: + DebugGuard.IsTrue( + bitsPerSample.Channels == 4 + && bitsPerSample.Channel3 == 24 + && bitsPerSample.Channel2 == 24 + && bitsPerSample.Channel1 == 24 + && bitsPerSample.Channel0 == 24, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new Rgba24242424TiffColor(extraSampleType, isBigEndian: byteOrder == ByteOrder.BigEndian); + case TiffColorType.Rgb323232: DebugGuard.IsTrue( bitsPerSample.Channels == 3 @@ -196,6 +348,17 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation DebugGuard.IsTrue(colorMap == null, "colorMap"); return new Rgb323232TiffColor(isBigEndian: byteOrder == ByteOrder.BigEndian); + case TiffColorType.Rgba32323232: + DebugGuard.IsTrue( + bitsPerSample.Channels == 4 + && bitsPerSample.Channel3 == 32 + && bitsPerSample.Channel2 == 32 + && bitsPerSample.Channel1 == 32 + && bitsPerSample.Channel0 == 32, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new Rgba32323232TiffColor(extraSampleType, isBigEndian: byteOrder == ByteOrder.BigEndian); + case TiffColorType.RgbFloat323232: DebugGuard.IsTrue( bitsPerSample.Channels == 3 @@ -206,6 +369,17 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation DebugGuard.IsTrue(colorMap == null, "colorMap"); return new RgbFloat323232TiffColor(isBigEndian: byteOrder == ByteOrder.BigEndian); + case TiffColorType.RgbaFloat32323232: + DebugGuard.IsTrue( + bitsPerSample.Channels == 4 + && bitsPerSample.Channel3 == 32 + && bitsPerSample.Channel2 == 32 + && bitsPerSample.Channel1 == 32 + && bitsPerSample.Channel0 == 32, + "bitsPerSample"); + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new RgbaFloat32323232TiffColor(isBigEndian: byteOrder == ByteOrder.BigEndian); + case TiffColorType.PaletteColor: DebugGuard.NotNull(colorMap, "colorMap"); return new PaletteTiffColor(bitsPerSample, colorMap); @@ -221,6 +395,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation public static TiffBasePlanarColorDecoder CreatePlanar( TiffColorType colorType, TiffBitsPerSample bitsPerSample, + TiffExtraSampleType? extraSampleType, ushort[] colorMap, Rational[] referenceBlackAndWhite, Rational[] ycbcrCoefficients, @@ -233,6 +408,10 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation DebugGuard.IsTrue(colorMap == null, "colorMap"); return new RgbPlanarTiffColor(bitsPerSample); + case TiffColorType.Rgba8888Planar: + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new RgbaPlanarTiffColor(extraSampleType, bitsPerSample); + case TiffColorType.YCbCrPlanar: return new YCbCrPlanarTiffColor(referenceBlackAndWhite, ycbcrCoefficients, ycbcrSubSampling); @@ -240,14 +419,26 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation DebugGuard.IsTrue(colorMap == null, "colorMap"); return new Rgb16PlanarTiffColor(byteOrder == ByteOrder.BigEndian); + case TiffColorType.Rgba16161616Planar: + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new Rgba16PlanarTiffColor(extraSampleType, byteOrder == ByteOrder.BigEndian); + case TiffColorType.Rgb242424Planar: DebugGuard.IsTrue(colorMap == null, "colorMap"); return new Rgb24PlanarTiffColor(byteOrder == ByteOrder.BigEndian); + case TiffColorType.Rgba24242424Planar: + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new Rgba24PlanarTiffColor(extraSampleType, byteOrder == ByteOrder.BigEndian); + case TiffColorType.Rgb323232Planar: DebugGuard.IsTrue(colorMap == null, "colorMap"); return new Rgb32PlanarTiffColor(byteOrder == ByteOrder.BigEndian); + case TiffColorType.Rgba32323232Planar: + DebugGuard.IsTrue(colorMap == null, "colorMap"); + return new Rgba32PlanarTiffColor(extraSampleType, byteOrder == ByteOrder.BigEndian); + default: throw TiffThrowHelper.InvalidColorType(colorType.ToString()); } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs index 8caefaed5..6b39224fb 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/TiffColorType.cs @@ -103,71 +103,171 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation /// Rgb222, + /// + /// RGBA color image with 2 bits for each channel. + /// + Rgba2222, + + /// + /// RGB color image with 3 bits for each channel. + /// + Rgb333, + + /// + /// RGBA color image with 3 bits for each channel. + /// + Rgba3333, + /// /// RGB color image with 4 bits for each channel. /// Rgb444, + /// + /// RGBA color image with 4 bits for each channel. + /// + Rgba4444, + + /// + /// RGB color image with 5 bits for each channel. + /// + Rgb555, + + /// + /// RGBA color image with 5 bits for each channel. + /// + Rgba5555, + + /// + /// RGB color image with 6 bits for each channel. + /// + Rgb666, + + /// + /// RGBA color image with 6 bits for each channel. + /// + Rgba6666, + /// /// RGB Full Color. Optimized implementation for 8-bit images. /// Rgb888, + /// + /// RGBA Full Color with 8-bit for each channel. + /// + Rgba8888, + /// /// RGB color image with 10 bits for each channel. /// Rgb101010, + /// + /// RGBA color image with 10 bits for each channel. + /// + Rgba10101010, + /// /// RGB color image with 12 bits for each channel. /// Rgb121212, + /// + /// RGBA color image with 12 bits for each channel. + /// + Rgba12121212, + /// /// RGB color image with 14 bits for each channel. /// Rgb141414, + /// + /// RGBA color image with 14 bits for each channel. + /// + Rgba14141414, + /// /// RGB color image with 16 bits for each channel. /// Rgb161616, + /// + /// RGBA color image with 16 bits for each channel. + /// + Rgba16161616, + /// /// RGB color image with 24 bits for each channel. /// Rgb242424, + /// + /// RGBA color image with 24 bits for each channel. + /// + Rgba24242424, + /// /// RGB color image with 32 bits for each channel. /// Rgb323232, + /// + /// RGBA color image with 32 bits for each channel. + /// + Rgba32323232, + /// /// RGB color image with 32 bits floats for each channel. /// RgbFloat323232, + /// + /// RGBA color image with 32 bits floats for each channel. + /// + RgbaFloat32323232, + /// /// RGB Full Color. Planar configuration of data. 8 Bit per color channel. /// Rgb888Planar, + /// + /// RGBA color image with an alpha channel. Planar configuration of data. 8 Bit per color channel. + /// + Rgba8888Planar, + /// /// RGB Full Color. Planar configuration of data. 16 Bit per color channel. /// Rgb161616Planar, + /// + /// RGB Color with an alpha channel. Planar configuration of data. 16 Bit per color channel. + /// + Rgba16161616Planar, + /// /// RGB Full Color. Planar configuration of data. 24 Bit per color channel. /// Rgb242424Planar, + /// + /// RGB Color with an alpha channel. Planar configuration of data. 24 Bit per color channel. + /// + Rgba24242424Planar, + /// /// RGB Full Color. Planar configuration of data. 32 Bit per color channel. /// Rgb323232Planar, + /// + /// RGB Color with an alpha channel. Planar configuration of data. 32 Bit per color channel. + /// + Rgba32323232Planar, + /// /// The pixels are stored in YCbCr format. /// diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero16TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero16TiffColor{TPixel}.cs index 038281c99..d509776d7 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero16TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero16TiffColor{TPixel}.cs @@ -29,7 +29,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 L16 l16 = TiffUtils.L16Default; var color = default(TPixel); - color.FromVector4(TiffUtils.Vector4Default); + color.FromScaledVector4(TiffUtils.Vector4Default); int offset = 0; for (int y = top; y < top + height; y++) diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero24TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero24TiffColor{TPixel}.cs index 807023b6b..fbf813078 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero24TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero24TiffColor{TPixel}.cs @@ -28,7 +28,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation // Note: due to an issue with netcore 2.1 and default values and unpredictable behavior with those, // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 var color = default(TPixel); - color.FromVector4(TiffUtils.Vector4Default); + color.FromScaledVector4(TiffUtils.Vector4Default); byte[] buffer = new byte[4]; int bufferStartIdx = this.isBigEndian ? 1 : 0; const uint maxValue = 0xFFFFFF; diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32FloatTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32FloatTiffColor{TPixel}.cs index 71323c7ba..40d1541c5 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32FloatTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32FloatTiffColor{TPixel}.cs @@ -29,7 +29,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation // Note: due to an issue with netcore 2.1 and default values and unpredictable behavior with those, // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 var color = default(TPixel); - color.FromVector4(TiffUtils.Vector4Default); + color.FromScaledVector4(TiffUtils.Vector4Default); byte[] buffer = new byte[4]; int offset = 0; @@ -46,7 +46,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation offset += 4; var colorVector = new Vector4(intensity, intensity, intensity, 1.0f); - color.FromVector4(colorVector); + color.FromScaledVector4(colorVector); pixelRow[x] = color; } } @@ -59,7 +59,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation offset += 4; var colorVector = new Vector4(intensity, intensity, intensity, 1.0f); - color.FromVector4(colorVector); + color.FromScaledVector4(colorVector); pixelRow[x] = color; } } diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32TiffColor{TPixel}.cs index e433956f0..fd908c1e9 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32TiffColor{TPixel}.cs @@ -28,7 +28,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation // Note: due to an issue with netcore 2.1 and default values and unpredictable behavior with those, // we define our own defaults as a workaround. See: https://github.com/dotnet/runtime/issues/55623 var color = default(TPixel); - color.FromVector4(TiffUtils.Vector4Default); + color.FromScaledVector4(TiffUtils.Vector4Default); const uint maxValue = 0xFFFFFFFF; int offset = 0; diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColor{TPixel}.cs index d692fc789..e1e2ba983 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColor{TPixel}.cs @@ -40,7 +40,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation int value = bitReader.ReadBits(this.bitsPerSample0); float intensity = 1.0f - (value / this.factor); - color.FromVector4(new Vector4(intensity, intensity, intensity, 1.0f)); + color.FromScaledVector4(new Vector4(intensity, intensity, intensity, 1.0f)); pixelRow[x] = color; } diff --git a/src/ImageSharp/Formats/Tiff/README.md b/src/ImageSharp/Formats/Tiff/README.md index ff4aa52b0..8cb327a7b 100644 --- a/src/ImageSharp/Formats/Tiff/README.md +++ b/src/ImageSharp/Formats/Tiff/README.md @@ -25,18 +25,9 @@ ## Implementation Status -- The Decoder currently only supports a single frame per image. +- The Decoder currently only supports decoding multiframe images, which have the same dimensions. - Some compression formats are not yet supported. See the list below. -### Deviations from the TIFF spec (to be fixed) - -- Decoder - - A Baseline TIFF reader must skip over extra components (e.g. RGB with 4 samples per pixels) - - NB: Need to handle this for both planar and chunky data - - If the SampleFormat field is present and not 1 - fail gracefully if you cannot handle this - - Compression=None should treat 16/32-BitsPerSample for all samples as SHORT/LONG (for byte order and padding rows) - - Check Planar format data - is this encoded as strips in order RGBRGBRGB or RRRGGGBBB? - ### Compression Formats | |Encoder|Decoder|Comments | @@ -45,7 +36,7 @@ |Ccitt1D | Y | Y | | |PackBits | Y | Y | | |CcittGroup3Fax | Y | Y | | -|CcittGroup4Fax | | Y | | +|CcittGroup4Fax | Y | Y | | |Lzw | Y | Y | Based on ImageSharp GIF LZW implementation - this code could be modified to be (i) shared, or (ii) optimised for each case | |Old Jpeg | | | We should not even try to support this | |Jpeg (Technote 2) | Y | Y | | @@ -87,7 +78,7 @@ |Model | Y | Y | | |StripOffsets | Y | Y | | |Orientation | | - | Ignore. Many readers ignore this tag. | -|SamplesPerPixel | Y | - | Currently ignored, as can be inferred from count of BitsPerSample | +|SamplesPerPixel | Y | - | Currently ignored, as can be inferred from count of BitsPerSample. | |RowsPerStrip | Y | Y | | |StripByteCounts | Y | Y | | |MinSampleValue | | | | @@ -105,7 +96,7 @@ |Artist | Y | Y | | |HostComputer | Y | Y | | |ColorMap | Y | Y | | -|ExtraSamples | | - | | +|ExtraSamples | | Y | Unspecified alpha data is not supported. | |Copyright | Y | Y | | ### Extension TIFF Tags diff --git a/src/ImageSharp/Formats/Tiff/TiffBitsPerSample.cs b/src/ImageSharp/Formats/Tiff/TiffBitsPerSample.cs index 8fd26ac13..9348a6883 100644 --- a/src/ImageSharp/Formats/Tiff/TiffBitsPerSample.cs +++ b/src/ImageSharp/Formats/Tiff/TiffBitsPerSample.cs @@ -25,6 +25,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// public readonly ushort Channel2; + /// + /// The bits for the alpha channel. + /// + public readonly ushort Channel3; + /// /// The number of channels. /// @@ -36,16 +41,19 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// The bits for the channel 0. /// The bits for the channel 1. /// The bits for the channel 2. - public TiffBitsPerSample(ushort channel0, ushort channel1, ushort channel2) + /// The bits for the channel 3. + public TiffBitsPerSample(ushort channel0, ushort channel1, ushort channel2, ushort channel3 = 0) { this.Channel0 = (ushort)Numerics.Clamp(channel0, 0, 32); this.Channel1 = (ushort)Numerics.Clamp(channel1, 0, 32); this.Channel2 = (ushort)Numerics.Clamp(channel2, 0, 32); + this.Channel3 = (ushort)Numerics.Clamp(channel3, 0, 32); this.Channels = 0; this.Channels += (byte)(this.Channel0 != 0 ? 1 : 0); this.Channels += (byte)(this.Channel1 != 0 ? 1 : 0); this.Channels += (byte)(this.Channel2 != 0 ? 1 : 0); + this.Channels += (byte)(this.Channel3 != 0 ? 1 : 0); } /// @@ -62,11 +70,19 @@ namespace SixLabors.ImageSharp.Formats.Tiff return false; } + ushort c3 = 0; ushort c2; ushort c1; ushort c0; switch (value.Length) { + case 4: + c3 = value[3]; + c2 = value[2]; + c1 = value[1]; + c0 = value[0]; + break; + case 3: c2 = value[2]; c1 = value[1]; @@ -84,7 +100,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff break; } - sample = new TiffBitsPerSample(c0, c1, c2); + sample = new TiffBitsPerSample(c0, c1, c2, c3); return true; } @@ -96,11 +112,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff public bool Equals(TiffBitsPerSample other) => this.Channel0 == other.Channel0 && this.Channel1 == other.Channel1 - && this.Channel2 == other.Channel2; + && this.Channel2 == other.Channel2 + && this.Channel3 == other.Channel3; /// public override int GetHashCode() - => HashCode.Combine(this.Channel0, this.Channel1, this.Channel2); + => HashCode.Combine(this.Channel0, this.Channel1, this.Channel2, this.Channel3); /// /// Converts the bits per sample struct to an ushort array. @@ -118,7 +135,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff return new[] { this.Channel0, this.Channel1 }; } - return new[] { this.Channel0, this.Channel1, this.Channel2 }; + if (this.Channel3 == 0) + { + return new[] { this.Channel0, this.Channel1, this.Channel2 }; + } + + return new[] { this.Channel0, this.Channel1, this.Channel2, this.Channel3 }; } /// @@ -127,12 +149,14 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// Bits per pixel. public TiffBitsPerPixel BitsPerPixel() { - int bitsPerPixel = this.Channel0 + this.Channel1 + this.Channel2; + int bitsPerPixel = this.Channel0 + this.Channel1 + this.Channel2 + this.Channel3; return (TiffBitsPerPixel)bitsPerPixel; } /// public override string ToString() - => $"TiffBitsPerSample({this.Channel0}, {this.Channel1}, {this.Channel2})"; + => this.Channel3 is 0 ? + $"TiffBitsPerSample({this.Channel0}, {this.Channel1}, {this.Channel2})" + : $"TiffBitsPerSample({this.Channel0}, {this.Channel1}, {this.Channel2}, {this.Channel3})"; } } diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoder.cs b/src/ImageSharp/Formats/Tiff/TiffDecoder.cs index 9d52e34df..c4a9f3b22 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoder.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoder.cs @@ -3,7 +3,7 @@ using System.IO; using System.Threading; -using System.Threading.Tasks; +using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Tiff @@ -18,50 +18,32 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// public bool IgnoreMetadata { get; set; } - /// - public Image Decode(Configuration configuration, Stream stream) - where TPixel : unmanaged, IPixel - { - Guard.NotNull(stream, "stream"); - - var decoder = new TiffDecoderCore(configuration, this); - return decoder.Decode(configuration, stream); - } - - /// - public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream); + /// + /// Gets or sets the decoding mode for multi-frame images. + /// + public FrameDecodingMode DecodingMode { get; set; } /// - public Task> DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) + public Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { Guard.NotNull(stream, nameof(stream)); var decoder = new TiffDecoderCore(configuration, this); - return decoder.DecodeAsync(configuration, stream, cancellationToken); + return decoder.Decode(configuration, stream, cancellationToken); } /// - public async Task DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) - => await this.DecodeAsync(configuration, stream, cancellationToken) - .ConfigureAwait(false); - - /// - public IImageInfo Identify(Configuration configuration, Stream stream) - { - Guard.NotNull(stream, nameof(stream)); - - var decoder = new TiffDecoderCore(configuration, this); - return decoder.Identify(configuration, stream); - } + public Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken) + => this.Decode(configuration, stream, cancellationToken); /// - public Task IdentifyAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) + public IImageInfo Identify(Configuration configuration, Stream stream, CancellationToken cancellationToken) { Guard.NotNull(stream, nameof(stream)); var decoder = new TiffDecoderCore(configuration, this); - return decoder.IdentifyAsync(configuration, stream, cancellationToken); + return decoder.Identify(configuration, stream, cancellationToken); } } } diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 05c5358f5..1cd3d2c0c 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -13,7 +13,6 @@ using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; -using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Tiff @@ -33,6 +32,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// private readonly bool ignoreMetadata; + /// + /// Gets the decoding mode for multi-frame images + /// + private readonly FrameDecodingMode decodingMode; + /// /// The stream to decode from. /// @@ -59,6 +63,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff this.Configuration = configuration ?? Configuration.Default; this.ignoreMetadata = options.IgnoreMetadata; + this.decodingMode = options.DecodingMode; this.memoryAllocator = this.Configuration.MemoryAllocator; } @@ -112,6 +117,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// public TiffFillOrder FillOrder { get; set; } + /// + /// Gets or sets the extra samples type. + /// + public TiffExtraSampleType? ExtraSamplesType { get; set; } + /// /// Gets or sets the JPEG tables when jpeg compression is used. /// @@ -147,35 +157,52 @@ namespace SixLabors.ImageSharp.Formats.Tiff public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - this.inputStream = stream; - var reader = new DirectoryReader(stream, this.Configuration.MemoryAllocator); - - IEnumerable directories = reader.Read(); - this.byteOrder = reader.ByteOrder; - this.isBigTiff = reader.IsBigTiff; - var frames = new List>(); - foreach (ExifProfile ifd in directories) + try { - cancellationToken.ThrowIfCancellationRequested(); - ImageFrame frame = this.DecodeFrame(ifd, cancellationToken); - frames.Add(frame); - } + this.inputStream = stream; + var reader = new DirectoryReader(stream, this.Configuration.MemoryAllocator); - ImageMetadata metadata = TiffDecoderMetadataCreator.Create(frames, this.ignoreMetadata, reader.ByteOrder, reader.IsBigTiff); + IEnumerable directories = reader.Read(); + this.byteOrder = reader.ByteOrder; + this.isBigTiff = reader.IsBigTiff; - // TODO: Tiff frames can have different sizes - ImageFrame root = frames[0]; - this.Dimensions = root.Size(); - foreach (ImageFrame frame in frames) - { - if (frame.Size() != root.Size()) + foreach (ExifProfile ifd in directories) { - TiffThrowHelper.ThrowNotSupported("Images with different sizes are not supported"); + cancellationToken.ThrowIfCancellationRequested(); + ImageFrame frame = this.DecodeFrame(ifd, cancellationToken); + frames.Add(frame); + + if (this.decodingMode is FrameDecodingMode.First) + { + break; + } } + + ImageMetadata metadata = TiffDecoderMetadataCreator.Create(frames, this.ignoreMetadata, reader.ByteOrder, reader.IsBigTiff); + + // TODO: Tiff frames can have different sizes. + ImageFrame root = frames[0]; + this.Dimensions = root.Size(); + foreach (ImageFrame frame in frames) + { + if (frame.Size() != root.Size()) + { + TiffThrowHelper.ThrowNotSupported("Images with different sizes are not supported"); + } + } + + return new Image(this.Configuration, metadata, frames); } + catch + { + foreach (ImageFrame f in frames) + { + f.Dispose(); + } - return new Image(this.Configuration, metadata, frames); + throw; + } } /// @@ -225,8 +252,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff var stripOffsetsArray = (Array)tags.GetValueInternal(ExifTag.StripOffsets).GetValue(); var stripByteCountsArray = (Array)tags.GetValueInternal(ExifTag.StripByteCounts).GetValue(); - IMemoryOwner stripOffsetsMemory = this.ConvertNumbers(stripOffsetsArray, out Span stripOffsets); - IMemoryOwner stripByteCountsMemory = this.ConvertNumbers(stripByteCountsArray, out Span stripByteCounts); + using IMemoryOwner stripOffsetsMemory = this.ConvertNumbers(stripOffsetsArray, out Span stripOffsets); + using IMemoryOwner stripByteCountsMemory = this.ConvertNumbers(stripByteCountsArray, out Span stripByteCounts); if (this.PlanarConfiguration == TiffPlanarConfiguration.Planar) { @@ -247,8 +274,6 @@ namespace SixLabors.ImageSharp.Formats.Tiff cancellationToken); } - stripOffsetsMemory?.Dispose(); - stripByteCountsMemory?.Dispose(); return frame; } @@ -265,12 +290,10 @@ namespace SixLabors.ImageSharp.Formats.Tiff return memory; } - else - { - DebugGuard.IsTrue(array is ulong[], $"Expected {nameof(UInt64)} array."); - span = (ulong[])array; - return null; - } + + DebugGuard.IsTrue(array is ulong[], $"Expected {nameof(UInt64)} array."); + span = (ulong[])array; + return null; } /// @@ -304,8 +327,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff case 2: bitsPerPixel = this.BitsPerSample.Channel2; break; + case 3: + bitsPerPixel = this.BitsPerSample.Channel2; + break; default: - TiffThrowHelper.ThrowNotSupported("More then 3 color channels are not supported"); + TiffThrowHelper.ThrowNotSupported("More then 4 color channels are not supported"); break; } } @@ -359,6 +385,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff TiffBasePlanarColorDecoder colorDecoder = TiffColorDecoderFactory.CreatePlanar( this.ColorType, this.BitsPerSample, + this.ExtraSamplesType, this.ColorMap, this.ReferenceBlackAndWhite, this.YcbcrCoefficients, @@ -440,6 +467,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff this.memoryAllocator, this.ColorType, this.BitsPerSample, + this.ExtraSamplesType, this.ColorMap, this.ReferenceBlackAndWhite, this.YcbcrCoefficients, diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs index a187d444a..6baf71466 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -29,9 +29,21 @@ namespace SixLabors.ImageSharp.Formats.Tiff TiffThrowHelper.ThrowNotSupported("Tiled images are not supported."); } - if (exifProfile.GetValueInternal(ExifTag.ExtraSamples) is not null) + IExifValue extraSamplesExifValue = exifProfile.GetValueInternal(ExifTag.ExtraSamples); + if (extraSamplesExifValue is not null) { - TiffThrowHelper.ThrowNotSupported("ExtraSamples is not supported."); + short[] extraSamples = (short[])extraSamplesExifValue.GetValue(); + if (extraSamples.Length != 1) + { + TiffThrowHelper.ThrowNotSupported("ExtraSamples is only supported with one extra sample for alpha data."); + } + + var extraSamplesType = (TiffExtraSampleType)extraSamples[0]; + options.ExtraSamplesType = extraSamplesType; + if (extraSamplesType is not (TiffExtraSampleType.UnassociatedAlphaData or TiffExtraSampleType.AssociatedAlphaData)) + { + TiffThrowHelper.ThrowNotSupported("Decoding Tiff images with ExtraSamples is not supported with UnspecifiedData."); + } } TiffFillOrder fillOrder = (TiffFillOrder?)exifProfile.GetValue(ExifTag.FillOrder)?.Value ?? TiffFillOrder.MostSignificantBitFirst; @@ -52,7 +64,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff sampleFormat = sampleFormats[0]; foreach (TiffSampleFormat format in sampleFormats) { - if (format != TiffSampleFormat.UnsignedInteger && format != TiffSampleFormat.Float) + if (format is not TiffSampleFormat.UnsignedInteger and not TiffSampleFormat.Float) { TiffThrowHelper.ThrowNotSupported("ImageSharp only supports the UnsignedInteger and Float SampleFormat."); } @@ -252,12 +264,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff case TiffPhotometricInterpretation.Rgb: { TiffBitsPerSample bitsPerSample = options.BitsPerSample; - if (bitsPerSample.Channels != 3) + if (bitsPerSample.Channels is not (3 or 4)) { TiffThrowHelper.ThrowNotSupported("The number of samples in the TIFF BitsPerSample entry is not supported."); } - if (!(bitsPerSample.Channel0 == bitsPerSample.Channel1 && bitsPerSample.Channel1 == bitsPerSample.Channel2)) + if ((bitsPerSample.Channels == 3 && !(bitsPerSample.Channel0 == bitsPerSample.Channel1 && bitsPerSample.Channel1 == bitsPerSample.Channel2)) || + (bitsPerSample.Channels == 4 && !(bitsPerSample.Channel0 == bitsPerSample.Channel1 && bitsPerSample.Channel1 == bitsPerSample.Channel2 && bitsPerSample.Channel2 == bitsPerSample.Channel3))) { TiffThrowHelper.ThrowNotSupported("Only BitsPerSample with equal bits per channel are supported."); } @@ -270,41 +283,50 @@ namespace SixLabors.ImageSharp.Formats.Tiff case 32: if (options.SampleFormat == TiffSampleFormat.Float) { - options.ColorType = TiffColorType.RgbFloat323232; + options.ColorType = options.BitsPerSample.Channels is 3 ? TiffColorType.RgbFloat323232 : TiffColorType.RgbaFloat32323232; return; } - options.ColorType = TiffColorType.Rgb323232; + options.ColorType = options.BitsPerSample.Channels is 3 ? TiffColorType.Rgb323232 : TiffColorType.Rgba32323232; break; case 24: - options.ColorType = TiffColorType.Rgb242424; + options.ColorType = options.BitsPerSample.Channels is 3 ? TiffColorType.Rgb242424 : TiffColorType.Rgba24242424; break; case 16: - options.ColorType = TiffColorType.Rgb161616; + options.ColorType = options.BitsPerSample.Channels is 3 ? TiffColorType.Rgb161616 : TiffColorType.Rgba16161616; break; case 14: - options.ColorType = TiffColorType.Rgb141414; + options.ColorType = options.BitsPerSample.Channels is 3 ? TiffColorType.Rgb141414 : TiffColorType.Rgba14141414; break; case 12: - options.ColorType = TiffColorType.Rgb121212; + options.ColorType = options.BitsPerSample.Channels is 3 ? TiffColorType.Rgb121212 : TiffColorType.Rgba12121212; break; case 10: - options.ColorType = TiffColorType.Rgb101010; + options.ColorType = options.BitsPerSample.Channels is 3 ? TiffColorType.Rgb101010 : TiffColorType.Rgba10101010; break; case 8: - options.ColorType = TiffColorType.Rgb888; + options.ColorType = options.BitsPerSample.Channels is 3 ? TiffColorType.Rgb888 : TiffColorType.Rgba8888; + break; + case 6: + options.ColorType = options.BitsPerSample.Channels is 3 ? TiffColorType.Rgb666 : TiffColorType.Rgba6666; + break; + case 5: + options.ColorType = options.BitsPerSample.Channels is 3 ? TiffColorType.Rgb555 : TiffColorType.Rgba5555; break; case 4: - options.ColorType = TiffColorType.Rgb444; + options.ColorType = options.BitsPerSample.Channels is 3 ? TiffColorType.Rgb444 : TiffColorType.Rgba4444; + break; + case 3: + options.ColorType = options.BitsPerSample.Channels is 3 ? TiffColorType.Rgb333 : TiffColorType.Rgba3333; break; case 2: - options.ColorType = TiffColorType.Rgb222; + options.ColorType = options.BitsPerSample.Channels is 3 ? TiffColorType.Rgb222 : TiffColorType.Rgba2222; break; default: TiffThrowHelper.ThrowNotSupported("Bits per sample is not supported."); @@ -317,16 +339,16 @@ namespace SixLabors.ImageSharp.Formats.Tiff switch (bitsPerChannel) { case 32: - options.ColorType = TiffColorType.Rgb323232Planar; + options.ColorType = options.BitsPerSample.Channels is 3 ? TiffColorType.Rgb323232Planar : TiffColorType.Rgba32323232Planar; break; case 24: - options.ColorType = TiffColorType.Rgb242424Planar; + options.ColorType = options.BitsPerSample.Channels is 3 ? TiffColorType.Rgb242424Planar : TiffColorType.Rgba24242424Planar; break; case 16: - options.ColorType = TiffColorType.Rgb161616Planar; + options.ColorType = options.BitsPerSample.Channels is 3 ? TiffColorType.Rgb161616Planar : TiffColorType.Rgba16161616Planar; break; default: - options.ColorType = TiffColorType.Rgb888Planar; + options.ColorType = options.BitsPerSample.Channels is 3 ? TiffColorType.Rgb888Planar : TiffColorType.Rgba8888Planar; break; } } diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs index e54d029ab..04f3682b0 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs @@ -395,6 +395,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff case TiffCompression.CcittGroup3Fax: return (ushort)TiffCompression.CcittGroup3Fax; + case TiffCompression.CcittGroup4Fax: + return (ushort)TiffCompression.CcittGroup4Fax; + case TiffCompression.Ccitt1D: return (ushort)TiffCompression.Ccitt1D; diff --git a/src/ImageSharp/Formats/Tiff/TiffExtraSampleType.cs b/src/ImageSharp/Formats/Tiff/TiffExtraSampleType.cs new file mode 100644 index 000000000..5fbc29177 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/TiffExtraSampleType.cs @@ -0,0 +1,27 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Tiff +{ + /// + /// Description of extra components. + /// + internal enum TiffExtraSampleType + { + /// + /// The data is unspecified, not supported. + /// + UnspecifiedData = 0, + + /// + /// The extra data is associated alpha data (with pre-multiplied color). + /// + AssociatedAlphaData = 1, + + /// + /// The extra data is unassociated alpha data is transparency information that logically exists independent of an image; + /// it is commonly called a soft matte. + /// + UnassociatedAlphaData = 2 + } +} diff --git a/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs b/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs index 4f71fa35c..532423c4f 100644 --- a/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs +++ b/src/ImageSharp/Formats/Tiff/Utils/TiffUtils.cs @@ -18,11 +18,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Utils private const float Scale32Bit = 1.0f / 0xFFFFFFFF; - public static Vector4 Vector4Default { get; } = new Vector4(0.0f, 0.0f, 0.0f, 0.0f); + public static Vector4 Vector4Default { get; } = new(0.0f, 0.0f, 0.0f, 0.0f); - public static Rgba64 Rgba64Default { get; } = new Rgba64(0, 0, 0, 0); + public static Rgba64 Rgba64Default { get; } = new(0, 0, 0, 0); - public static L16 L16Default { get; } = new L16(0); + public static L16 L16Default { get; } = new(0); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ushort ConvertToUShortBigEndian(ReadOnlySpan buffer) => BinaryPrimitives.ReadUInt16BigEndian(buffer); @@ -46,7 +46,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Utils } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static TPixel ColorFromRgba64(Rgba64 rgba, ulong r, ulong g, ulong b, TPixel color) + public static TPixel ColorFromRgb64(Rgba64 rgba, ulong r, ulong g, ulong b, TPixel color) where TPixel : unmanaged, IPixel { rgba.PackedValue = r | (g << 16) | (b << 32) | (0xfffful << 48); @@ -54,24 +54,76 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Utils return color; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel ColorFromRgba64(Rgba64 rgba, ulong r, ulong g, ulong b, ulong a, TPixel color) + where TPixel : unmanaged, IPixel + { + rgba.PackedValue = r | (g << 16) | (b << 32) | (a << 48); + color.FromRgba64(rgba); + return color; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel ColorFromRgba64Premultiplied(Rgba64 rgba, ulong r, ulong g, ulong b, ulong a, TPixel color) + where TPixel : unmanaged, IPixel + { + rgba.PackedValue = r | (g << 16) | (b << 32) | (a << 48); + var vec = rgba.ToVector4(); + return UnPremultiply(ref vec, color); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static TPixel ColorScaleTo24Bit(ulong r, ulong g, ulong b, TPixel color) where TPixel : unmanaged, IPixel { var colorVector = new Vector4(r * Scale24Bit, g * Scale24Bit, b * Scale24Bit, 1.0f); - color.FromVector4(colorVector); + color.FromScaledVector4(colorVector); + return color; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel ColorScaleTo24Bit(ulong r, ulong g, ulong b, ulong a, TPixel color) + where TPixel : unmanaged, IPixel + { + Vector4 colorVector = new Vector4(r, g, b, a) * Scale24Bit; + color.FromScaledVector4(colorVector); return color; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel ColorScaleTo24BitPremultiplied(ulong r, ulong g, ulong b, ulong a, TPixel color) + where TPixel : unmanaged, IPixel + { + Vector4 colorVector = new Vector4(r, g, b, a) * Scale24Bit; + return UnPremultiply(ref colorVector, color); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static TPixel ColorScaleTo32Bit(ulong r, ulong g, ulong b, TPixel color) where TPixel : unmanaged, IPixel { var colorVector = new Vector4(r * Scale32Bit, g * Scale32Bit, b * Scale32Bit, 1.0f); - color.FromVector4(colorVector); + color.FromScaledVector4(colorVector); + return color; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel ColorScaleTo32Bit(ulong r, ulong g, ulong b, ulong a, TPixel color) + where TPixel : unmanaged, IPixel + { + Vector4 colorVector = new Vector4(r, g, b, a) * Scale32Bit; + color.FromScaledVector4(colorVector); return color; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel ColorScaleTo32BitPremultiplied(ulong r, ulong g, ulong b, ulong a, TPixel color) + where TPixel : unmanaged, IPixel + { + Vector4 colorVector = new Vector4(r, g, b, a) * Scale32Bit; + return UnPremultiply(ref colorVector, color); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static TPixel ColorFromL16(L16 l16, ushort intensity, TPixel color) where TPixel : unmanaged, IPixel @@ -86,7 +138,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Utils where TPixel : unmanaged, IPixel { var colorVector = new Vector4(intensity * Scale24Bit, intensity * Scale24Bit, intensity * Scale24Bit, 1.0f); - color.FromVector4(colorVector); + color.FromScaledVector4(colorVector); return color; } @@ -95,7 +147,17 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Utils where TPixel : unmanaged, IPixel { var colorVector = new Vector4(intensity * Scale32Bit, intensity * Scale32Bit, intensity * Scale32Bit, 1.0f); - color.FromVector4(colorVector); + color.FromScaledVector4(colorVector); + return color; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel UnPremultiply(ref Vector4 vector, TPixel color) + where TPixel : unmanaged, IPixel + { + Numerics.UnPremultiply(ref vector); + color.FromScaledVector4(vector); + return color; } @@ -112,8 +174,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Utils return 0; } - int padding = subSampling - (valueToRoundUp % subSampling); - return padding; + return subSampling - (valueToRoundUp % subSampling); } } } diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs index 5fec09ef1..f21a465cd 100644 --- a/src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs @@ -37,7 +37,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers { int width = this.Image.Width; - if (compressor.Method == TiffCompression.CcittGroup3Fax || compressor.Method == TiffCompression.Ccitt1D) + if (compressor.Method == TiffCompression.CcittGroup3Fax || compressor.Method == TiffCompression.Ccitt1D || compressor.Method == TiffCompression.CcittGroup4Fax) { // Special case for T4BitCompressor. int stripPixels = width * height; diff --git a/src/ImageSharp/Formats/Webp/AlphaEncoder.cs b/src/ImageSharp/Formats/Webp/AlphaEncoder.cs new file mode 100644 index 000000000..1019073d8 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/AlphaEncoder.cs @@ -0,0 +1,133 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Formats.Webp.Lossless; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Webp +{ + /// + /// Methods for encoding the alpha data of a VP8 image. + /// + internal class AlphaEncoder : IDisposable + { + private IMemoryOwner alphaData; + + /// + /// Encodes the alpha channel data. + /// Data is either compressed as lossless webp image or uncompressed. + /// + /// The pixel format. + /// The to encode from. + /// The global configuration. + /// The memory manager. + /// Indicates, if the data should be compressed with the lossless webp compression. + /// The size in bytes of the alpha data. + /// The encoded alpha data. + public IMemoryOwner EncodeAlpha(Image image, Configuration configuration, MemoryAllocator memoryAllocator, bool compress, out int size) + where TPixel : unmanaged, IPixel + { + int width = image.Width; + int height = image.Height; + this.alphaData = ExtractAlphaChannel(image, configuration, memoryAllocator); + + if (compress) + { + WebpEncodingMethod effort = WebpEncodingMethod.Default; + int quality = 8 * (int)effort; + using var lossLessEncoder = new Vp8LEncoder( + memoryAllocator, + configuration, + width, + height, + quality, + effort, + WebpTransparentColorMode.Preserve, + false, + 0); + + // The transparency information will be stored in the green channel of the ARGB quadruplet. + // The green channel is allowed extra transformation steps in the specification -- unlike the other channels, + // that can improve compression. + using Image alphaAsImage = DispatchAlphaToGreen(image, this.alphaData.GetSpan()); + + size = lossLessEncoder.EncodeAlphaImageData(alphaAsImage, this.alphaData); + + return this.alphaData; + } + + size = width * height; + return this.alphaData; + } + + /// + /// Store the transparency in the green channel. + /// + /// The pixel format. + /// The to encode from. + /// A byte sequence of length width * height, containing all the 8-bit transparency values in scan order. + /// The transparency image. + private static Image DispatchAlphaToGreen(Image image, Span alphaData) + where TPixel : unmanaged, IPixel + { + int width = image.Width; + int height = image.Height; + var alphaAsImage = new Image(width, height); + + for (int y = 0; y < height; y++) + { + Memory rowBuffer = alphaAsImage.DangerousGetPixelRowMemory(y); + Span pixelRow = rowBuffer.Span; + Span alphaRow = alphaData.Slice(y * width, width); + for (int x = 0; x < width; x++) + { + // Leave A/R/B channels zero'd. + pixelRow[x] = new Rgba32(0, alphaRow[x], 0, 0); + } + } + + return alphaAsImage; + } + + /// + /// Extract the alpha data of the image. + /// + /// The pixel format. + /// The to encode from. + /// The global configuration. + /// The memory manager. + /// A byte sequence of length width * height, containing all the 8-bit transparency values in scan order. + private static IMemoryOwner ExtractAlphaChannel(Image image, Configuration configuration, MemoryAllocator memoryAllocator) + where TPixel : unmanaged, IPixel + { + Buffer2D imageBuffer = image.Frames.RootFrame.PixelBuffer; + int height = image.Height; + int width = image.Width; + IMemoryOwner alphaDataBuffer = memoryAllocator.Allocate(width * height); + Span alphaData = alphaDataBuffer.GetSpan(); + + using IMemoryOwner rowBuffer = memoryAllocator.Allocate(width); + Span rgbaRow = rowBuffer.GetSpan(); + + for (int y = 0; y < height; y++) + { + Span rowSpan = imageBuffer.DangerousGetRowSpan(y); + PixelOperations.Instance.ToRgba32(configuration, rowSpan, rgbaRow); + int offset = y * width; + for (int x = 0; x < width; x++) + { + alphaData[offset + x] = rgbaRow[x].A; + } + } + + return alphaDataBuffer; + } + + /// + public void Dispose() => this.alphaData?.Dispose(); + } +} diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs index ac039be79..0e1c9f4d9 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -5,6 +5,7 @@ using System; using System.Buffers.Binary; using System.IO; using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; namespace SixLabors.ImageSharp.Formats.Webp.BitWriter @@ -47,6 +48,12 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter /// The stream to write to. public void WriteToStream(Stream stream) => stream.Write(this.Buffer.AsSpan(0, this.NumBytes())); + /// + /// Writes the encoded bytes of the image to the given buffer. Call Finish() before this. + /// + /// The destination buffer. + public void WriteToBuffer(Span dest) => this.Buffer.AsSpan(0, this.NumBytes()).CopyTo(dest); + /// /// Resizes the buffer to write to. /// @@ -91,10 +98,10 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter } /// - /// Calculates the chunk size of EXIF or XMP metadata. + /// Calculates the chunk size of EXIF, XMP or ICCP metadata. /// /// The metadata profile bytes. - /// The exif chunk size in bytes. + /// The metadata chunk size in bytes. protected uint MetadataChunkSize(byte[] metadataBytes) { uint metaSize = (uint)metadataBytes.Length; @@ -103,6 +110,19 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter return metaChunkSize; } + /// + /// Calculates the chunk size of a alpha chunk. + /// + /// The alpha chunk bytes. + /// The alpha data chunk size in bytes. + protected uint AlphaChunkSize(Span alphaBytes) + { + uint alphaSize = (uint)alphaBytes.Length + 1; + uint alphaChunkSize = WebpConstants.ChunkHeaderSize + alphaSize + (alphaSize & 1); + + return alphaChunkSize; + } + /// /// Writes a metadata profile (EXIF or XMP) to the stream. /// @@ -128,16 +148,72 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter } } + /// + /// Writes the alpha chunk to the stream. + /// + /// The stream to write to. + /// The alpha channel data bytes. + /// Indicates, if the alpha channel data is compressed. + protected void WriteAlphaChunk(Stream stream, Span dataBytes, bool alphaDataIsCompressed) + { + uint size = (uint)dataBytes.Length + 1; + Span buf = this.scratchBuffer.AsSpan(0, 4); + BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Alpha); + stream.Write(buf); + BinaryPrimitives.WriteUInt32LittleEndian(buf, size); + stream.Write(buf); + + byte flags = 0; + if (alphaDataIsCompressed) + { + flags |= 1; + } + + stream.WriteByte(flags); + stream.Write(dataBytes); + + // Add padding byte if needed. + if ((size & 1) == 1) + { + stream.WriteByte(0); + } + } + + /// + /// Writes the color profile to the stream. + /// + /// The stream to write to. + /// The color profile bytes. + protected void WriteColorProfile(Stream stream, byte[] iccProfileBytes) + { + uint size = (uint)iccProfileBytes.Length; + + Span buf = this.scratchBuffer.AsSpan(0, 4); + BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Iccp); + stream.Write(buf); + BinaryPrimitives.WriteUInt32LittleEndian(buf, size); + stream.Write(buf); + + stream.Write(iccProfileBytes); + + // Add padding byte if needed. + if ((size & 1) == 1) + { + stream.WriteByte(0); + } + } + /// /// Writes a VP8X header to the stream. /// /// The stream to write to. /// A exif profile or null, if it does not exist. /// A XMP profile or null, if it does not exist. + /// The color profile bytes. /// The width of the image. /// The height of the image. /// Flag indicating, if a alpha channel is present. - protected void WriteVp8XHeader(Stream stream, ExifProfile exifProfile, XmpProfile xmpProfile, uint width, uint height, bool hasAlpha) + protected void WriteVp8XHeader(Stream stream, ExifProfile exifProfile, XmpProfile xmpProfile, byte[] iccProfileBytes, uint width, uint height, bool hasAlpha) { if (width > MaxDimension || height > MaxDimension) { @@ -169,6 +245,12 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter flags |= 16; } + if (iccProfileBytes != null) + { + // Set iccp flag. + flags |= 32; + } + Span buf = this.scratchBuffer.AsSpan(0, 4); stream.Write(WebpConstants.Vp8XMagicBytes); BinaryPrimitives.WriteUInt32LittleEndian(buf, WebpConstants.Vp8XChunkSize); diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs index 4e91bedb0..7cc915e18 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs @@ -6,6 +6,7 @@ using System.Buffers.Binary; using System.IO; using SixLabors.ImageSharp.Formats.Webp.Lossy; using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; namespace SixLabors.ImageSharp.Formats.Webp.BitWriter @@ -406,19 +407,31 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter /// The stream to write to. /// The exif profile. /// The XMP profile. + /// The color profile. /// The width of the image. /// The height of the image. /// Flag indicating, if a alpha channel is present. - public void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, XmpProfile xmpProfile, uint width, uint height, bool hasAlpha) + /// The alpha channel data. + /// Indicates, if the alpha data is compressed. + public void WriteEncodedImageToStream( + Stream stream, + ExifProfile exifProfile, + XmpProfile xmpProfile, + IccProfile iccProfile, + uint width, + uint height, + bool hasAlpha, + Span alphaData, + bool alphaDataIsCompressed) { bool isVp8X = false; byte[] exifBytes = null; byte[] xmpBytes = null; + byte[] iccProfileBytes = null; uint riffSize = 0; if (exifProfile != null) { isVp8X = true; - riffSize += ExtendedFileChunkSize; exifBytes = exifProfile.ToByteArray(); riffSize += this.MetadataChunkSize(exifBytes); } @@ -426,11 +439,28 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter if (xmpProfile != null) { isVp8X = true; - riffSize += ExtendedFileChunkSize; xmpBytes = xmpProfile.Data; riffSize += this.MetadataChunkSize(xmpBytes); } + if (iccProfile != null) + { + isVp8X = true; + iccProfileBytes = iccProfile.ToByteArray(); + riffSize += this.MetadataChunkSize(iccProfileBytes); + } + + if (hasAlpha) + { + isVp8X = true; + riffSize += this.AlphaChunkSize(alphaData); + } + + if (isVp8X) + { + riffSize += ExtendedFileChunkSize; + } + this.Finish(); uint numBytes = (uint)this.NumBytes(); int mbSize = this.enc.Mbw * this.enc.Mbh; @@ -438,7 +468,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter var bitWriterPartZero = new Vp8BitWriter(expectedSize); - // Partition #0 with header and partition sizes + // Partition #0 with header and partition sizes. uint size0 = this.GeneratePartition0(bitWriterPartZero); uint vp8Size = WebpConstants.Vp8FrameHeaderSize + size0; @@ -446,12 +476,12 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter uint pad = vp8Size & 1; vp8Size += pad; - // Compute RIFF size + // Compute RIFF size. // At the minimum it is: "WEBPVP8 nnnn" + VP8 data size. riffSize += WebpConstants.TagSize + WebpConstants.ChunkHeaderSize + vp8Size; // Emit headers and partition #0 - this.WriteWebpHeaders(stream, size0, vp8Size, riffSize, isVp8X, width, height, exifProfile, xmpProfile, hasAlpha); + this.WriteWebpHeaders(stream, size0, vp8Size, riffSize, isVp8X, width, height, exifProfile, xmpProfile, iccProfileBytes, hasAlpha, alphaData, alphaDataIsCompressed); bitWriterPartZero.WriteToStream(stream); // Write the encoded image to the stream. @@ -639,14 +669,37 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter while (it.Next()); } - private void WriteWebpHeaders(Stream stream, uint size0, uint vp8Size, uint riffSize, bool isVp8X, uint width, uint height, ExifProfile exifProfile, XmpProfile xmpProfile, bool hasAlpha) + private void WriteWebpHeaders( + Stream stream, + uint size0, + uint vp8Size, + uint riffSize, + bool isVp8X, + uint width, + uint height, + ExifProfile exifProfile, + XmpProfile xmpProfile, + byte[] iccProfileBytes, + bool hasAlpha, + Span alphaData, + bool alphaDataIsCompressed) { this.WriteRiffHeader(stream, riffSize); // Write VP8X, header if necessary. if (isVp8X) { - this.WriteVp8XHeader(stream, exifProfile, xmpProfile, width, height, hasAlpha); + this.WriteVp8XHeader(stream, exifProfile, xmpProfile, iccProfileBytes, width, height, hasAlpha); + + if (iccProfileBytes != null) + { + this.WriteColorProfile(stream, iccProfileBytes); + } + + if (hasAlpha) + { + this.WriteAlphaChunk(stream, alphaData, alphaDataIsCompressed); + } } this.WriteVp8Header(stream, vp8Size); diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs index d41224f90..7bd6febeb 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs @@ -6,6 +6,7 @@ using System.Buffers.Binary; using System.IO; using SixLabors.ImageSharp.Formats.Webp.Lossless; using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; namespace SixLabors.ImageSharp.Formats.Webp.BitWriter @@ -134,19 +135,20 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter /// The stream to write to. /// The exif profile. /// The XMP profile. + /// The color profile. /// The width of the image. /// The height of the image. /// Flag indicating, if a alpha channel is present. - public void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, XmpProfile xmpProfile, uint width, uint height, bool hasAlpha) + public void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, XmpProfile xmpProfile, IccProfile iccProfile, uint width, uint height, bool hasAlpha) { bool isVp8X = false; byte[] exifBytes = null; byte[] xmpBytes = null; + byte[] iccBytes = null; uint riffSize = 0; if (exifProfile != null) { isVp8X = true; - riffSize += ExtendedFileChunkSize; exifBytes = exifProfile.ToByteArray(); riffSize += this.MetadataChunkSize(exifBytes); } @@ -154,11 +156,22 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter if (xmpProfile != null) { isVp8X = true; - riffSize += ExtendedFileChunkSize; xmpBytes = xmpProfile.Data; riffSize += this.MetadataChunkSize(xmpBytes); } + if (iccProfile != null) + { + isVp8X = true; + iccBytes = iccProfile.ToByteArray(); + riffSize += this.MetadataChunkSize(iccBytes); + } + + if (isVp8X) + { + riffSize += ExtendedFileChunkSize; + } + this.Finish(); uint size = (uint)this.NumBytes(); size++; // One byte extra for the VP8L signature. @@ -171,7 +184,12 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter // Write VP8X, header if necessary. if (isVp8X) { - this.WriteVp8XHeader(stream, exifProfile, xmpProfile, width, height, hasAlpha); + this.WriteVp8XHeader(stream, exifProfile, xmpProfile, iccBytes, width, height, hasAlpha); + + if (iccBytes != null) + { + this.WriteColorProfile(stream, iccBytes); + } } // Write magic bytes indicating its a lossless webp. diff --git a/src/ImageSharp/Formats/Webp/IWebpEncoderOptions.cs b/src/ImageSharp/Formats/Webp/IWebpEncoderOptions.cs index d119d3031..57ec32753 100644 --- a/src/ImageSharp/Formats/Webp/IWebpEncoderOptions.cs +++ b/src/ImageSharp/Formats/Webp/IWebpEncoderOptions.cs @@ -31,6 +31,7 @@ namespace SixLabors.ImageSharp.Formats.Webp /// /// Gets a value indicating whether the alpha plane should be compressed with Webp lossless format. + /// Defaults to true. /// bool UseAlphaCompression { get; } diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index e9dce913a..8de1ccc42 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -228,7 +228,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless public Vp8LHashChain HashChain { get; } /// - /// Encodes the image to the specified stream from the . + /// Encodes the image as lossless webp to the specified stream. /// /// The pixel format. /// The to encode from. @@ -236,10 +236,12 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless public void Encode(Image image, Stream stream) where TPixel : unmanaged, IPixel { - image.Metadata.SyncProfiles(); int width = image.Width; int height = image.Height; + ImageMetadata metadata = image.Metadata; + metadata.SyncProfiles(); + // Convert image pixels to bgra array. bool hasAlpha = this.ConvertPixelsToBgra(image, width, height); @@ -253,9 +255,40 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless this.EncodeStream(image); // Write bytes from the bitwriter buffer to the stream. - ImageMetadata metadata = image.Metadata; - metadata.SyncProfiles(); - this.bitWriter.WriteEncodedImageToStream(stream, metadata.ExifProfile, metadata.XmpProfile, (uint)width, (uint)height, hasAlpha); + this.bitWriter.WriteEncodedImageToStream(stream, metadata.ExifProfile, metadata.XmpProfile, metadata.IccProfile, (uint)width, (uint)height, hasAlpha); + } + + /// + /// Encodes the alpha image data using the webp lossless compression. + /// + /// The type of the pixel. + /// The to encode from. + /// The destination buffer to write the encoded alpha data to. + /// The size of the compressed data in bytes. + /// If the size of the data is the same as the pixel count, the compression would not yield in smaller data and is left uncompressed. + /// + public int EncodeAlphaImageData(Image image, IMemoryOwner alphaData) + where TPixel : unmanaged, IPixel + { + int width = image.Width; + int height = image.Height; + int pixelCount = width * height; + + // Convert image pixels to bgra array. + this.ConvertPixelsToBgra(image, width, height); + + // The image-stream will NOT contain any headers describing the image dimension, the dimension is already known. + this.EncodeStream(image); + this.bitWriter.Finish(); + int size = this.bitWriter.NumBytes(); + if (size >= pixelCount) + { + // Compressing would not yield in smaller data -> leave the data uncompressed. + return pixelCount; + } + + this.bitWriter.WriteToBuffer(alphaData.GetSpan()); + return size; } /// diff --git a/src/ImageSharp/Formats/Webp/Lossy/QuantEnc.cs b/src/ImageSharp/Formats/Webp/Lossy/QuantEnc.cs index 2a40cffdc..b6703d739 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/QuantEnc.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/QuantEnc.cs @@ -16,8 +16,6 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy /// internal static unsafe class QuantEnc { - private static readonly byte[] Zigzag = { 0, 1, 4, 8, 5, 2, 3, 6, 9, 12, 13, 10, 7, 11, 14, 15 }; - private static readonly ushort[] WeightY = { 38, 32, 20, 9, 32, 28, 17, 7, 20, 17, 10, 4, 9, 7, 4, 2 }; private const int MaxLevel = 2047; @@ -47,6 +45,9 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy private const int DSHIFT = 4; private const int DSCALE = 1; // storage descaling, needed to make the error fit byte + // This uses C#'s optimization to refer to the static data segment of the assembly, no allocation occurs. + private static ReadOnlySpan Zigzag => new byte[] { 0, 1, 4, 8, 5, 2, 3, 6, 9, 12, 13, 10, 7, 11, 14, 15 }; + public static void PickBestIntra16(Vp8EncIterator it, ref Vp8ModeScore rd, Vp8SegmentInfo[] segmentInfos, Vp8EncProba proba) { const int numBlocks = 16; diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs index eb8d92110..9470a94d9 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8EncIterator.cs @@ -874,7 +874,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy this.SetCountDown(this.mbw * this.mbh); this.InitTop(); - Array.Clear(this.BitCount, 0, this.BitCount.Length); + Array.Clear(this.BitCount); } /// diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index 022232050..f24fdb45d 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -71,12 +71,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy /// private int uvAlpha; - /// - /// Scratch buffer to reduce allocations. - /// - private readonly int[] scratch = new int[16]; - - private readonly byte[] averageBytesPerMb = { 50, 24, 16, 9, 7, 5, 3, 2 }; + private readonly bool alphaCompression; private const int NumMbSegments = 4; @@ -105,6 +100,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy /// Number of entropy-analysis passes (in [1..10]). /// The filter the strength of the deblocking filter, between 0 (no filtering) and 100 (maximum filtering). /// The spatial noise shaping. 0=off, 100=maximum. + /// If true, the alpha channel will be compressed with the lossless compression. public Vp8Encoder( MemoryAllocator memoryAllocator, Configuration configuration, @@ -114,7 +110,8 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy WebpEncodingMethod method, int entropyPasses, int filterStrength, - int spatialNoiseShaping) + int spatialNoiseShaping, + bool alphaCompression) { this.memoryAllocator = memoryAllocator; this.configuration = configuration; @@ -125,6 +122,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy this.entropyPasses = Numerics.Clamp(entropyPasses, 1, 10); this.filterStrength = Numerics.Clamp(filterStrength, 0, 100); this.spatialNoiseShaping = Numerics.Clamp(spatialNoiseShaping, 0, 100); + this.alphaCompression = alphaCompression; this.rdOptLevel = method is WebpEncodingMethod.BestQuality ? Vp8RdLevel.RdOptTrellisAll : method >= WebpEncodingMethod.Level5 ? Vp8RdLevel.RdOptTrellis : method >= WebpEncodingMethod.Level3 ? Vp8RdLevel.RdOptBasic @@ -174,6 +172,9 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy this.ResetBoundaryPredictions(); } + // This uses C#'s optimization to refer to the static data segment of the assembly, no allocation occurs. + private static ReadOnlySpan AverageBytesPerMb => new byte[] { 50, 24, 16, 9, 7, 5, 3, 2 }; + public int BaseQuant { get; set; } /// @@ -297,10 +298,11 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy { int width = image.Width; int height = image.Height; + int pixelCount = width * height; Span y = this.Y.GetSpan(); Span u = this.U.GetSpan(); Span v = this.V.GetSpan(); - YuvConversion.ConvertRgbToYuv(image, this.configuration, this.memoryAllocator, y, u, v); + bool hasAlpha = YuvConversion.ConvertRgbToYuv(image, this.configuration, this.memoryAllocator, y, u, v); int yStride = width; int uvStride = (yStride + 1) >> 1; @@ -318,12 +320,26 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy this.SetLoopParams(this.quality); // Initialize the bitwriter. - int averageBytesPerMacroBlock = this.averageBytesPerMb[this.BaseQuant >> 4]; + int averageBytesPerMacroBlock = AverageBytesPerMb[this.BaseQuant >> 4]; int expectedSize = this.Mbw * this.Mbh * averageBytesPerMacroBlock; this.bitWriter = new Vp8BitWriter(expectedSize, this); - // TODO: EncodeAlpha(); - bool hasAlpha = false; + // Extract and encode alpha channel data, if present. + int alphaDataSize = 0; + bool alphaCompressionSucceeded = false; + using var alphaEncoder = new AlphaEncoder(); + Span alphaData = Span.Empty; + if (hasAlpha) + { + // TODO: This can potentially run in an separate task. + IMemoryOwner encodedAlphaData = alphaEncoder.EncodeAlpha(image, this.configuration, this.memoryAllocator, this.alphaCompression, out alphaDataSize); + alphaData = encodedAlphaData.GetSpan(); + if (alphaDataSize < pixelCount) + { + // Only use compressed data, if the compressed data is actually smaller then the uncompressed data. + alphaCompressionSucceeded = true; + } + } // Stats-collection loop. this.StatLoop(width, height, yStride, uvStride); @@ -358,7 +374,16 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy // Write bytes from the bitwriter buffer to the stream. ImageMetadata metadata = image.Metadata; metadata.SyncProfiles(); - this.bitWriter.WriteEncodedImageToStream(stream, metadata.ExifProfile, metadata.XmpProfile, (uint)width, (uint)height, hasAlpha); + this.bitWriter.WriteEncodedImageToStream( + stream, + metadata.ExifProfile, + metadata.XmpProfile, + metadata.IccProfile, + (uint)width, + (uint)height, + hasAlpha, + alphaData, + this.alphaCompression && alphaCompressionSucceeded); } /// diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Matrix.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Matrix.cs index 66c91e44a..7ee7a7008 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Matrix.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Matrix.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using System; + namespace SixLabors.ImageSharp.Formats.Webp.Lossy { internal unsafe struct Vp8Matrix @@ -13,10 +15,6 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy new[] { 110, 115 } }; - // Sharpening by (slightly) raising the hi-frequency coeffs. - // Hack-ish but helpful for mid-bitrate range. Use with care. - private static readonly byte[] FreqSharpening = { 0, 30, 60, 90, 30, 60, 90, 90, 60, 90, 90, 90, 90, 90, 90, 90 }; - /// /// Number of descaling bits for sharpening bias. /// @@ -47,6 +45,11 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy /// public fixed short Sharpen[16]; + // Sharpening by (slightly) raising the hi-frequency coeffs. + // Hack-ish but helpful for mid-bitrate range. Use with care. + // This uses C#'s optimization to refer to the static data segment of the assembly, no allocation occurs. + private static ReadOnlySpan FreqSharpening => new byte[] { 0, 30, 60, 90, 30, 60, 90, 90, 60, 90, 90, 90, 90, 90, 90, 90 }; + /// /// Returns the average quantizer. /// diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8ModeScore.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8ModeScore.cs index 69841b557..72a3e971a 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8ModeScore.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8ModeScore.cs @@ -97,11 +97,11 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy public void Clear() { - Array.Clear(this.YDcLevels, 0, this.YDcLevels.Length); - Array.Clear(this.YAcLevels, 0, this.YAcLevels.Length); - Array.Clear(this.UvLevels, 0, this.UvLevels.Length); - Array.Clear(this.ModesI4, 0, this.ModesI4.Length); - Array.Clear(this.Derr, 0, this.Derr.Length); + Array.Clear(this.YDcLevels); + Array.Clear(this.YAcLevels); + Array.Clear(this.UvLevels); + Array.Clear(this.ModesI4); + Array.Clear(this.Derr); } public void InitScore() diff --git a/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs b/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs index 7a731f428..d0374d637 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs @@ -159,7 +159,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy private static void UpSampleSse41(Span topY, Span bottomY, Span topU, Span topV, Span curU, Span curV, Span topDst, Span bottomDst, int len, byte[] uvBuffer) { const int xStep = 3; - Array.Clear(uvBuffer, 0, uvBuffer.Length); + Array.Clear(uvBuffer); Span ru = uvBuffer.AsSpan(15); Span rv = ru.Slice(32); @@ -318,7 +318,8 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy /// Span to store the luma component of the image. /// Span to store the u component of the image. /// Span to store the v component of the image. - public static void ConvertRgbToYuv(Image image, Configuration configuration, MemoryAllocator memoryAllocator, Span y, Span u, Span v) + /// true, if the image contains alpha data. + public static bool ConvertRgbToYuv(Image image, Configuration configuration, MemoryAllocator memoryAllocator, Span y, Span u, Span v) where TPixel : unmanaged, IPixel { Buffer2D imageBuffer = image.Frames.RootFrame.PixelBuffer; @@ -335,6 +336,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy Span bgraRow1 = bgraRow1Buffer.GetSpan(); int uvRowIndex = 0; int rowIndex; + bool hasAlpha = false; for (rowIndex = 0; rowIndex < height - 1; rowIndex += 2) { Span rowSpan = imageBuffer.DangerousGetRowSpan(rowIndex); @@ -343,6 +345,10 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy PixelOperations.Instance.ToBgra32(configuration, nextRowSpan, bgraRow1); bool rowsHaveAlpha = WebpCommonUtils.CheckNonOpaque(bgraRow0) && WebpCommonUtils.CheckNonOpaque(bgraRow1); + if (rowsHaveAlpha) + { + hasAlpha = true; + } // Downsample U/V planes, two rows at a time. if (!rowsHaveAlpha) @@ -375,10 +381,13 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy else { AccumulateRgba(bgraRow0, bgraRow0, tmpRgbSpan, width); + hasAlpha = true; } ConvertRgbaToUv(tmpRgbSpan, u.Slice(uvRowIndex * uvWidth), v.Slice(uvRowIndex * uvWidth), uvWidth); } + + return hasAlpha; } /// diff --git a/src/ImageSharp/Formats/Webp/WebpDecoder.cs b/src/ImageSharp/Formats/Webp/WebpDecoder.cs index b4e6cecd0..c9470a66f 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoder.cs @@ -3,8 +3,6 @@ using System.IO; using System.Threading; -using System.Threading.Tasks; -using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -21,7 +19,7 @@ namespace SixLabors.ImageSharp.Formats.Webp public bool IgnoreMetadata { get; set; } /// - public Image Decode(Configuration configuration, Stream stream) + public Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { Guard.NotNull(stream, nameof(stream)); @@ -30,39 +28,7 @@ namespace SixLabors.ImageSharp.Formats.Webp try { - return decoder.Decode(configuration, stream); - } - catch (InvalidMemoryOperationException ex) - { - Size dims = decoder.Dimensions; - - throw new InvalidImageContentException($"Cannot decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}.", ex); - } - } - - /// - public IImageInfo Identify(Configuration configuration, Stream stream) - { - Guard.NotNull(stream, nameof(stream)); - - return new WebpDecoderCore(configuration, this).Identify(configuration, stream); - } - - /// - public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream); - - /// - public Task> DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel - { - Guard.NotNull(stream, nameof(stream)); - - var decoder = new WebpDecoderCore(configuration, this); - - try - { - using var bufferedStream = new BufferedReadStream(configuration, stream); - return decoder.DecodeAsync(configuration, bufferedStream, cancellationToken); + return decoder.Decode(configuration, stream, cancellationToken); } catch (InvalidMemoryOperationException ex) { @@ -73,16 +39,15 @@ namespace SixLabors.ImageSharp.Formats.Webp } /// - public async Task DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) - => await this.DecodeAsync(configuration, stream, cancellationToken).ConfigureAwait(false); + public Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken) + => this.Decode(configuration, stream, cancellationToken); - /// - public Task IdentifyAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) + /// + public IImageInfo Identify(Configuration configuration, Stream stream, CancellationToken cancellationToken) { Guard.NotNull(stream, nameof(stream)); - using var bufferedStream = new BufferedReadStream(configuration, stream); - return new WebpDecoderCore(configuration, this).IdentifyAsync(configuration, bufferedStream, cancellationToken); + return new WebpDecoderCore(configuration, this).Identify(configuration, stream, cancellationToken); } } } diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs index 9d18e5d82..0e00f037c 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs @@ -82,38 +82,47 @@ namespace SixLabors.ImageSharp.Formats.Webp public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - this.Metadata = new ImageMetadata(); - this.currentStream = stream; + Image image = null; + try + { + this.Metadata = new ImageMetadata(); + this.currentStream = stream; - uint fileSize = this.ReadImageHeader(); + uint fileSize = this.ReadImageHeader(); - using (this.webImageInfo = this.ReadVp8Info()) - { - if (this.webImageInfo.Features is { Animation: true }) + using (this.webImageInfo = this.ReadVp8Info()) { - WebpThrowHelper.ThrowNotSupportedException("Animations are not supported"); - } + if (this.webImageInfo.Features is { Animation: true }) + { + WebpThrowHelper.ThrowNotSupportedException("Animations are not supported"); + } - var image = new Image(this.Configuration, (int)this.webImageInfo.Width, (int)this.webImageInfo.Height, this.Metadata); - Buffer2D pixels = image.GetRootFramePixelBuffer(); - if (this.webImageInfo.IsLossless) - { - var losslessDecoder = new WebpLosslessDecoder(this.webImageInfo.Vp8LBitReader, this.memoryAllocator, this.Configuration); - losslessDecoder.Decode(pixels, image.Width, image.Height); - } - else - { - var lossyDecoder = new WebpLossyDecoder(this.webImageInfo.Vp8BitReader, this.memoryAllocator, this.Configuration); - lossyDecoder.Decode(pixels, image.Width, image.Height, this.webImageInfo); - } + image = new Image(this.Configuration, (int)this.webImageInfo.Width, (int)this.webImageInfo.Height, this.Metadata); + Buffer2D pixels = image.GetRootFramePixelBuffer(); + if (this.webImageInfo.IsLossless) + { + var losslessDecoder = new WebpLosslessDecoder(this.webImageInfo.Vp8LBitReader, this.memoryAllocator, this.Configuration); + losslessDecoder.Decode(pixels, image.Width, image.Height); + } + else + { + var lossyDecoder = new WebpLossyDecoder(this.webImageInfo.Vp8BitReader, this.memoryAllocator, this.Configuration); + lossyDecoder.Decode(pixels, image.Width, image.Height, this.webImageInfo); + } - // There can be optional chunks after the image data, like EXIF and XMP. - if (this.webImageInfo.Features != null) - { - this.ParseOptionalChunks(this.webImageInfo.Features); - } + // There can be optional chunks after the image data, like EXIF and XMP. + if (this.webImageInfo.Features != null) + { + this.ParseOptionalChunks(this.webImageInfo.Features); + } - return image; + return image; + } + } + catch + { + image?.Dispose(); + throw; } } @@ -190,7 +199,11 @@ namespace SixLabors.ImageSharp.Formats.Webp uint fileSize = this.ReadChunkSize(); // The first byte contains information about the image features used. - byte imageFeatures = (byte)this.currentStream.ReadByte(); + int imageFeatures = this.currentStream.ReadByte(); + if (imageFeatures == -1) + { + WebpThrowHelper.ThrowInvalidImageContentException("VP8X header doe not contain enough data"); + } // The first two bit of it are reserved and should be 0. if (imageFeatures >> 6 != 0) @@ -214,19 +227,34 @@ namespace SixLabors.ImageSharp.Formats.Webp features.Animation = (imageFeatures & (1 << 1)) != 0; // 3 reserved bytes should follow which are supposed to be zero. - this.currentStream.Read(this.buffer, 0, 3); + int bytesRead = this.currentStream.Read(this.buffer, 0, 3); + if (bytesRead != 3) + { + WebpThrowHelper.ThrowInvalidImageContentException("VP8X header does not contain enough data"); + } + if (this.buffer[0] != 0 || this.buffer[1] != 0 || this.buffer[2] != 0) { WebpThrowHelper.ThrowImageFormatException("reserved bytes should be zero"); } // 3 bytes for the width. - this.currentStream.Read(this.buffer, 0, 3); + bytesRead = this.currentStream.Read(this.buffer, 0, 3); + if (bytesRead != 3) + { + WebpThrowHelper.ThrowInvalidImageContentException("VP8 header does not contain enough data to read the width"); + } + this.buffer[3] = 0; uint width = (uint)BinaryPrimitives.ReadInt32LittleEndian(this.buffer) + 1; // 3 bytes for the height. - this.currentStream.Read(this.buffer, 0, 3); + bytesRead = this.currentStream.Read(this.buffer, 0, 3); + if (bytesRead != 3) + { + WebpThrowHelper.ThrowInvalidImageContentException("VP8 header does not contain enough data to read the height"); + } + this.buffer[3] = 0; uint height = (uint)BinaryPrimitives.ReadInt32LittleEndian(this.buffer) + 1; @@ -272,7 +300,12 @@ namespace SixLabors.ImageSharp.Formats.Webp this.webpMetadata.FileFormat = WebpFileFormatType.Lossy; // VP8 data size (not including this 4 bytes). - this.currentStream.Read(this.buffer, 0, 4); + int bytesRead = this.currentStream.Read(this.buffer, 0, 4); + if (bytesRead != 4) + { + WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the VP8 data size"); + } + uint dataSize = BinaryPrimitives.ReadUInt32LittleEndian(this.buffer); // remaining counts the available image data payload. @@ -284,7 +317,12 @@ namespace SixLabors.ImageSharp.Formats.Webp // - A 3-bit version number. // - A 1-bit show_frame flag. // - A 19-bit field containing the size of the first data partition in bytes. - this.currentStream.Read(this.buffer, 0, 3); + bytesRead = this.currentStream.Read(this.buffer, 0, 3); + if (bytesRead != 3) + { + WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the VP8 frame tag"); + } + uint frameTag = (uint)(this.buffer[0] | (this.buffer[1] << 8) | (this.buffer[2] << 16)); remaining -= 3; bool isNoKeyFrame = (frameTag & 0x1) == 1; @@ -312,13 +350,23 @@ namespace SixLabors.ImageSharp.Formats.Webp } // Check for VP8 magic bytes. - this.currentStream.Read(this.buffer, 0, 3); + bytesRead = this.currentStream.Read(this.buffer, 0, 3); + if (bytesRead != 3) + { + WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the VP8 magic bytes"); + } + if (!this.buffer.AsSpan(0, 3).SequenceEqual(WebpConstants.Vp8HeaderMagicBytes)) { WebpThrowHelper.ThrowImageFormatException("VP8 magic bytes not found"); } - this.currentStream.Read(this.buffer, 0, 4); + bytesRead = this.currentStream.Read(this.buffer, 0, 4); + if (bytesRead != 4) + { + WebpThrowHelper.ThrowInvalidImageContentException("VP8 header does not contain enough data to read the image width and height"); + } + uint tmp = (uint)BinaryPrimitives.ReadInt16LittleEndian(this.buffer); uint width = tmp & 0x3fff; sbyte xScale = (sbyte)(tmp >> 6); @@ -429,54 +477,15 @@ namespace SixLabors.ImageSharp.Formats.Webp switch (chunkType) { case WebpChunkType.Iccp: - uint iccpChunkSize = this.ReadChunkSize(); - if (this.IgnoreMetadata) - { - this.currentStream.Skip((int)iccpChunkSize); - } - else - { - byte[] iccpData = new byte[iccpChunkSize]; - this.currentStream.Read(iccpData, 0, (int)iccpChunkSize); - var profile = new IccProfile(iccpData); - if (profile.CheckIsValid()) - { - this.Metadata.IccProfile = profile; - } - } - + this.ReadIccProfile(); break; case WebpChunkType.Exif: - uint exifChunkSize = this.ReadChunkSize(); - if (this.IgnoreMetadata) - { - this.currentStream.Skip((int)exifChunkSize); - } - else - { - byte[] exifData = new byte[exifChunkSize]; - this.currentStream.Read(exifData, 0, (int)exifChunkSize); - var profile = new ExifProfile(exifData); - this.Metadata.ExifProfile = profile; - } - + this.ReadExifProfile(); break; case WebpChunkType.Xmp: - uint xmpChunkSize = this.ReadChunkSize(); - if (this.IgnoreMetadata) - { - this.currentStream.Skip((int)xmpChunkSize); - } - else - { - byte[] xmpData = new byte[xmpChunkSize]; - this.currentStream.Read(xmpData, 0, (int)xmpChunkSize); - var profile = new XmpProfile(xmpData); - this.Metadata.XmpProfile = profile; - } - + this.ReadXmpProfile(); break; case WebpChunkType.Animation: @@ -488,7 +497,12 @@ namespace SixLabors.ImageSharp.Formats.Webp features.AlphaChunkHeader = (byte)this.currentStream.ReadByte(); int alphaDataSize = (int)(alphaChunkSize - 1); features.AlphaData = this.memoryAllocator.Allocate(alphaDataSize); - this.currentStream.Read(features.AlphaData.Memory.Span, 0, alphaDataSize); + int bytesRead = this.currentStream.Read(features.AlphaData.Memory.Span, 0, alphaDataSize); + if (bytesRead != alphaDataSize) + { + WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the alpha chunk"); + } + break; default: WebpThrowHelper.ThrowImageFormatException("Unexpected chunk followed VP8X header"); @@ -514,22 +528,100 @@ namespace SixLabors.ImageSharp.Formats.Webp { // Read chunk header. WebpChunkType chunkType = this.ReadChunkType(); - uint chunkLength = this.ReadChunkSize(); - if (chunkType == WebpChunkType.Exif && this.Metadata.ExifProfile == null) { - byte[] exifData = new byte[chunkLength]; - this.currentStream.Read(exifData, 0, (int)chunkLength); - this.Metadata.ExifProfile = new ExifProfile(exifData); + this.ReadExifProfile(); + } + else if (chunkType == WebpChunkType.Xmp && this.Metadata.XmpProfile == null) + { + this.ReadXmpProfile(); } else { - // Skip XMP chunk data or any duplicate EXIF chunk. + // Skip duplicate XMP or EXIF chunk. + uint chunkLength = this.ReadChunkSize(); this.currentStream.Skip((int)chunkLength); } } } + /// + /// Reads the EXIF profile from the stream. + /// + private void ReadExifProfile() + { + uint exifChunkSize = this.ReadChunkSize(); + if (this.IgnoreMetadata) + { + this.currentStream.Skip((int)exifChunkSize); + } + else + { + byte[] exifData = new byte[exifChunkSize]; + int bytesRead = this.currentStream.Read(exifData, 0, (int)exifChunkSize); + if (bytesRead != exifChunkSize) + { + // Ignore invalid chunk. + return; + } + + var profile = new ExifProfile(exifData); + this.Metadata.ExifProfile = profile; + } + } + + /// + /// Reads the XMP profile the stream. + /// + private void ReadXmpProfile() + { + uint xmpChunkSize = this.ReadChunkSize(); + if (this.IgnoreMetadata) + { + this.currentStream.Skip((int)xmpChunkSize); + } + else + { + byte[] xmpData = new byte[xmpChunkSize]; + int bytesRead = this.currentStream.Read(xmpData, 0, (int)xmpChunkSize); + if (bytesRead != xmpChunkSize) + { + // Ignore invalid chunk. + return; + } + + var profile = new XmpProfile(xmpData); + this.Metadata.XmpProfile = profile; + } + } + + /// + /// Reads the ICCP chunk from the stream. + /// + private void ReadIccProfile() + { + uint iccpChunkSize = this.ReadChunkSize(); + if (this.IgnoreMetadata) + { + this.currentStream.Skip((int)iccpChunkSize); + } + else + { + byte[] iccpData = new byte[iccpChunkSize]; + int bytesRead = this.currentStream.Read(iccpData, 0, (int)iccpChunkSize); + if (bytesRead != iccpChunkSize) + { + WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the iccp chunk"); + } + + var profile = new IccProfile(iccpData); + if (profile.CheckIsValid()) + { + this.Metadata.IccProfile = profile; + } + } + } + /// /// Identifies the chunk type from the chunk. /// diff --git a/src/ImageSharp/Formats/Webp/WebpEncoder.cs b/src/ImageSharp/Formats/Webp/WebpEncoder.cs index bdcbb194b..d0b60d18c 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoder.cs @@ -24,7 +24,7 @@ namespace SixLabors.ImageSharp.Formats.Webp public WebpEncodingMethod Method { get; set; } = WebpEncodingMethod.Default; /// - public bool UseAlphaCompression { get; set; } + public bool UseAlphaCompression { get; set; } = true; /// public int EntropyPasses { get; set; } = 1; diff --git a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs index 195fa62bd..0fbff81fe 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoderCore.cs @@ -22,8 +22,8 @@ namespace SixLabors.ImageSharp.Formats.Webp private readonly MemoryAllocator memoryAllocator; /// - /// TODO: not used at the moment. /// Indicating whether the alpha plane should be compressed with Webp lossless format. + /// Defaults to true. /// private readonly bool alphaCompression; @@ -100,7 +100,7 @@ namespace SixLabors.ImageSharp.Formats.Webp } /// - /// Encodes the image to the specified stream from the . + /// Encodes the image as webp to the specified stream. /// /// The pixel format. /// The to encode from. @@ -149,7 +149,8 @@ namespace SixLabors.ImageSharp.Formats.Webp this.method, this.entropyPasses, this.filterStrength, - this.spatialNoiseShaping); + this.spatialNoiseShaping, + this.alphaCompression); enc.Encode(image, stream); } } diff --git a/src/ImageSharp/Formats/Webp/WebpThrowHelper.cs b/src/ImageSharp/Formats/Webp/WebpThrowHelper.cs index fffdd3410..5f063fd11 100644 --- a/src/ImageSharp/Formats/Webp/WebpThrowHelper.cs +++ b/src/ImageSharp/Formats/Webp/WebpThrowHelper.cs @@ -8,6 +8,13 @@ namespace SixLabors.ImageSharp.Formats.Webp { internal static class WebpThrowHelper { + /// + /// Cold path optimization for throwing 's. + /// + /// The error message for the exception. + [MethodImpl(InliningOptions.ColdPath)] + public static void ThrowInvalidImageContentException(string errorMessage) => throw new InvalidImageContentException(errorMessage); + /// /// Cold path optimization for throwing -s /// diff --git a/src/ImageSharp/IO/BufferedReadStream.cs b/src/ImageSharp/IO/BufferedReadStream.cs index 4ab7f312b..2823b8ed6 100644 --- a/src/ImageSharp/IO/BufferedReadStream.cs +++ b/src/ImageSharp/IO/BufferedReadStream.cs @@ -114,6 +114,15 @@ namespace SixLabors.ImageSharp.IO /// public override bool CanWrite { get; } = false; + /// + /// Gets remaining byte count available to read. + /// + public long RemainingBytes + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.Length - this.Position; + } + /// /// Gets the underlying stream. /// diff --git a/src/ImageSharp/IO/ChunkedMemoryStream.cs b/src/ImageSharp/IO/ChunkedMemoryStream.cs index e39930def..a1ea2b760 100644 --- a/src/ImageSharp/IO/ChunkedMemoryStream.cs +++ b/src/ImageSharp/IO/ChunkedMemoryStream.cs @@ -16,19 +16,17 @@ namespace SixLabors.ImageSharp.IO /// internal sealed class ChunkedMemoryStream : Stream { - /// - /// The default length in bytes of each buffer chunk. - /// - public const int DefaultBufferLength = 128 * 1024; - // The memory allocator. private readonly MemoryAllocator allocator; // Data private MemoryChunk memoryChunk; - // The length of each buffer chunk - private readonly int chunkLength; + // The total number of allocated chunks + private int chunkCount; + + // The length of the largest contiguous buffer that can be handled by the allocator. + private readonly int allocatorCapacity; // Has the stream been disposed. private bool isDisposed; @@ -49,21 +47,10 @@ namespace SixLabors.ImageSharp.IO /// Initializes a new instance of the class. /// public ChunkedMemoryStream(MemoryAllocator allocator) - : this(DefaultBufferLength, allocator) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The length, in bytes of each buffer chunk. - /// The memory allocator. - public ChunkedMemoryStream(int bufferLength, MemoryAllocator allocator) { - Guard.MustBeGreaterThan(bufferLength, 0, nameof(bufferLength)); Guard.NotNull(allocator, nameof(allocator)); - this.chunkLength = bufferLength; + this.allocatorCapacity = allocator.GetBufferCapacityInBytes(); this.allocator = allocator; } @@ -191,6 +178,9 @@ namespace SixLabors.ImageSharp.IO case SeekOrigin.End: this.Position = this.Length + offset; break; + default: + ThrowInvalidSeek(); + break; } return this.Position; @@ -219,6 +209,7 @@ namespace SixLabors.ImageSharp.IO this.memoryChunk = null; this.writeChunk = null; this.readChunk = null; + this.chunkCount = 0; } finally { @@ -519,17 +510,22 @@ namespace SixLabors.ImageSharp.IO } [MethodImpl(MethodImplOptions.NoInlining)] - private static void ThrowDisposed() - => throw new ObjectDisposedException(null, "The stream is closed."); + private static void ThrowDisposed() => throw new ObjectDisposedException(null, "The stream is closed."); [MethodImpl(MethodImplOptions.NoInlining)] - private static void ThrowArgumentOutOfRange(string value) - => throw new ArgumentOutOfRangeException(value); + private static void ThrowArgumentOutOfRange(string value) => throw new ArgumentOutOfRangeException(value); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowInvalidSeek() => throw new ArgumentException("Invalid seek origin."); [MethodImpl(MethodImplOptions.AggressiveInlining)] private MemoryChunk AllocateMemoryChunk() { - IMemoryOwner buffer = this.allocator.Allocate(this.chunkLength); + // Tweak our buffer sizes to take the minimum of the provided buffer sizes + // or the allocator buffer capacity which provides us with the largest + // available contiguous buffer size. + IMemoryOwner buffer = this.allocator.Allocate(Math.Min(this.allocatorCapacity, GetChunkSize(this.chunkCount++))); + return new MemoryChunk { Buffer = buffer, @@ -547,6 +543,18 @@ namespace SixLabors.ImageSharp.IO } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetChunkSize(int i) + { + // Increment chunks sizes with moderate speed, but without using too many buffers from the same ArrayPool bucket of the default MemoryAllocator. + // https://github.com/SixLabors/ImageSharp/pull/2006#issuecomment-1066244720 +#pragma warning disable IDE1006 // Naming Styles + const int _128K = 1 << 17; + const int _4M = 1 << 22; + return i < 16 ? _128K * (1 << (i / 4)) : _4M; +#pragma warning restore IDE1006 // Naming Styles + } + private sealed class MemoryChunk : IDisposable { private bool isDisposed; diff --git a/src/ImageSharp/Image.Decode.cs b/src/ImageSharp/Image.Decode.cs index ee340bf86..3c2b6d67f 100644 --- a/src/ImageSharp/Image.Decode.cs +++ b/src/ImageSharp/Image.Decode.cs @@ -2,11 +2,8 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Buffers; using System.IO; -using System.Linq; using System.Threading; -using System.Threading.Tasks; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; @@ -25,7 +22,7 @@ namespace SixLabors.ImageSharp /// The image might be filled with memory garbage. /// /// The pixel type - /// The + /// The /// The width of the image /// The height of the image /// The @@ -37,8 +34,10 @@ namespace SixLabors.ImageSharp ImageMetadata metadata) where TPixel : unmanaged, IPixel { - Buffer2D uninitializedMemoryBuffer = - configuration.MemoryAllocator.Allocate2D(width, height); + Buffer2D uninitializedMemoryBuffer = configuration.MemoryAllocator.Allocate2D( + width, + height, + configuration.PreferContiguousImageBuffers); return new Image(configuration, uninitializedMemoryBuffer.FastMemoryGroup, width, height, metadata); } @@ -96,20 +95,6 @@ namespace SixLabors.ImageSharp return format; } - /// - /// By reading the header on the provided stream this calculates the images format. - /// - /// The image stream to read the header from. - /// The configuration. - /// The mime type or null if none found. - private static Task InternalDetectFormatAsync(Stream stream, Configuration config) - { - // We are going to cheat here because we know that by this point we have been wrapped in a - // seekable stream then we are free to use sync APIs this is potentially brittle and may - // need a better fix in the future. - return Task.FromResult(InternalDetectFormat(stream, config)); - } - /// /// By reading the header on the provided stream this calculates the images format. /// @@ -126,33 +111,17 @@ namespace SixLabors.ImageSharp : null; } - /// - /// By reading the header on the provided stream this calculates the images format. - /// - /// The image stream to read the header from. - /// The configuration. - /// The decoder and the image format or null if none found. - private static async Task<(IImageDecoder Decoder, IImageFormat Format)> DiscoverDecoderAsync(Stream stream, Configuration config) - { - IImageFormat format = await InternalDetectFormatAsync(stream, config).ConfigureAwait(false); - - IImageDecoder decoder = format != null - ? config.ImageFormatsManager.FindDecoder(format) - : null; - - return (decoder, format); - } - /// /// Decodes the image stream to the current image. /// /// The stream. /// the configuration. + /// The token to monitor for cancellation requests. /// The pixel format. /// /// A new . /// - private static (Image Image, IImageFormat Format) Decode(Stream stream, Configuration config) + private static (Image Image, IImageFormat Format) Decode(Stream stream, Configuration config, CancellationToken cancellationToken = default) where TPixel : unmanaged, IPixel { IImageDecoder decoder = DiscoverDecoder(stream, config, out IImageFormat format); @@ -161,37 +130,11 @@ namespace SixLabors.ImageSharp return (null, null); } - Image img = decoder.Decode(config, stream); - return (img, format); - } - - /// - /// Decodes the image stream to the current image. - /// - /// The stream. - /// the configuration. - /// The token to monitor for cancellation requests. - /// The pixel format. - /// A representing the asynchronous operation. - private static async Task<(Image Image, IImageFormat Format)> DecodeAsync( - Stream stream, - Configuration config, - CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel - { - (IImageDecoder decoder, IImageFormat format) = await DiscoverDecoderAsync(stream, config) - .ConfigureAwait(false); - if (decoder is null) - { - return (null, null); - } - - Image img = await decoder.DecodeAsync(config, stream, cancellationToken) - .ConfigureAwait(false); + Image img = decoder.Decode(config, stream, cancellationToken); return (img, format); } - private static (Image Image, IImageFormat Format) Decode(Stream stream, Configuration config) + private static (Image Image, IImageFormat Format) Decode(Stream stream, Configuration config, CancellationToken cancellationToken = default) { IImageDecoder decoder = DiscoverDecoder(stream, config, out IImageFormat format); if (decoder is null) @@ -199,19 +142,7 @@ namespace SixLabors.ImageSharp return (null, null); } - Image img = decoder.Decode(config, stream); - return (img, format); - } - - private static async Task<(Image Image, IImageFormat Format)> DecodeAsync(Stream stream, Configuration config, CancellationToken cancellationToken) - { - (IImageDecoder decoder, IImageFormat format) = await DiscoverDecoderAsync(stream, config).ConfigureAwait(false); - if (decoder is null) - { - return (null, null); - } - - Image img = await decoder.DecodeAsync(config, stream, cancellationToken).ConfigureAwait(false); + Image img = decoder.Decode(config, stream, cancellationToken); return (img, format); } @@ -220,47 +151,20 @@ namespace SixLabors.ImageSharp /// /// The stream. /// the configuration. + /// The token to monitor for cancellation requests. /// /// The or null if a suitable info detector is not found. /// - private static (IImageInfo ImageInfo, IImageFormat Format) InternalIdentity(Stream stream, Configuration config) + private static (IImageInfo ImageInfo, IImageFormat Format) InternalIdentity(Stream stream, Configuration config, CancellationToken cancellationToken = default) { IImageDecoder decoder = DiscoverDecoder(stream, config, out IImageFormat format); - if (!(decoder is IImageInfoDetector detector)) - { - return (null, null); - } - - IImageInfo info = detector?.Identify(config, stream); - return (info, format); - } - - /// - /// Reads the raw image information from the specified stream. - /// - /// The stream. - /// the configuration. - /// The token to monitor for cancellation requests. - /// - /// A representing the asynchronous operation with the - /// property of the returned type set to null if a suitable detector - /// is not found. - private static async Task<(IImageInfo ImageInfo, IImageFormat Format)> InternalIdentityAsync(Stream stream, Configuration config, CancellationToken cancellationToken) - { - (IImageDecoder decoder, IImageFormat format) = await DiscoverDecoderAsync(stream, config).ConfigureAwait(false); - - if (!(decoder is IImageInfoDetector detector)) + if (decoder is not IImageInfoDetector detector) { return (null, null); } - if (detector is null) - { - return (null, format); - } - - IImageInfo info = await detector.IdentifyAsync(config, stream, cancellationToken).ConfigureAwait(false); + IImageInfo info = detector?.Identify(config, stream, cancellationToken); return (info, format); } } diff --git a/src/ImageSharp/Image.FromFile.cs b/src/ImageSharp/Image.FromFile.cs index fce0835fb..345a5e6c8 100644 --- a/src/ImageSharp/Image.FromFile.cs +++ b/src/ImageSharp/Image.FromFile.cs @@ -298,7 +298,7 @@ namespace SixLabors.ImageSharp /// Image format is not supported. /// Image contains invalid content. /// A representing the asynchronous operation. - public static Task LoadAsync( + public static async Task LoadAsync( Configuration configuration, string path, IImageDecoder decoder, @@ -308,7 +308,8 @@ namespace SixLabors.ImageSharp Guard.NotNull(path, nameof(path)); using Stream stream = configuration.FileSystem.OpenRead(path); - return LoadAsync(configuration, stream, decoder, cancellationToken); + return await LoadAsync(configuration, stream, decoder, cancellationToken) + .ConfigureAwait(false); } /// @@ -326,7 +327,7 @@ namespace SixLabors.ImageSharp /// Image contains invalid content. /// The pixel format. /// A representing the asynchronous operation. - public static Task> LoadAsync( + public static async Task> LoadAsync( Configuration configuration, string path, IImageDecoder decoder, @@ -337,7 +338,8 @@ namespace SixLabors.ImageSharp Guard.NotNull(path, nameof(path)); using Stream stream = configuration.FileSystem.OpenRead(path); - return LoadAsync(configuration, stream, decoder, cancellationToken); + return await LoadAsync(configuration, stream, decoder, cancellationToken) + .ConfigureAwait(false); } /// diff --git a/src/ImageSharp/Image.FromStream.cs b/src/ImageSharp/Image.FromStream.cs index f5e32d8ce..e10d7fe3d 100644 --- a/src/ImageSharp/Image.FromStream.cs +++ b/src/ImageSharp/Image.FromStream.cs @@ -65,7 +65,7 @@ namespace SixLabors.ImageSharp => WithSeekableStreamAsync( configuration, stream, - (s, _) => InternalDetectFormatAsync(s, configuration), + (s, _) => InternalDetectFormat(s, configuration), cancellationToken); /// @@ -208,7 +208,7 @@ namespace SixLabors.ImageSharp => WithSeekableStreamAsync( configuration, stream, - (s, ct) => InternalIdentityAsync(s, configuration ?? Configuration.Default, ct), + (s, ct) => InternalIdentity(s, configuration ?? Configuration.Default, ct), cancellationToken); /// @@ -313,7 +313,7 @@ namespace SixLabors.ImageSharp public static Image Load(Configuration configuration, Stream stream, IImageDecoder decoder) { Guard.NotNull(decoder, nameof(decoder)); - return WithSeekableStream(configuration, stream, s => decoder.Decode(configuration, s)); + return WithSeekableStream(configuration, stream, s => decoder.Decode(configuration, s, default)); } /// @@ -341,7 +341,7 @@ namespace SixLabors.ImageSharp return WithSeekableStreamAsync( configuration, stream, - (s, ct) => decoder.DecodeAsync(configuration, s, ct), + (s, ct) => decoder.Decode(configuration, s, ct), cancellationToken); } @@ -432,9 +432,9 @@ namespace SixLabors.ImageSharp /// Image contains invalid content. /// The pixel format. /// A representing the asynchronous operation. - public static async Task<(Image Image, IImageFormat Format)> LoadWithFormatAsync(Stream stream, CancellationToken cancellationToken = default) + public static Task<(Image Image, IImageFormat Format)> LoadWithFormatAsync(Stream stream, CancellationToken cancellationToken = default) where TPixel : unmanaged, IPixel - => await LoadWithFormatAsync(Configuration.Default, stream, cancellationToken).ConfigureAwait(false); + => LoadWithFormatAsync(Configuration.Default, stream, cancellationToken); /// /// Create a new instance of the class from the given stream. @@ -449,7 +449,7 @@ namespace SixLabors.ImageSharp /// A new . public static Image Load(Stream stream, IImageDecoder decoder) where TPixel : unmanaged, IPixel - => WithSeekableStream(Configuration.Default, stream, s => decoder.Decode(Configuration.Default, s)); + => WithSeekableStream(Configuration.Default, stream, s => decoder.Decode(Configuration.Default, s, default)); /// /// Create a new instance of the class from the given stream. @@ -468,7 +468,7 @@ namespace SixLabors.ImageSharp => WithSeekableStreamAsync( Configuration.Default, stream, - (s, ct) => decoder.DecodeAsync(Configuration.Default, s, ct), + (s, ct) => decoder.Decode(Configuration.Default, s, ct), cancellationToken); /// @@ -486,7 +486,7 @@ namespace SixLabors.ImageSharp /// A new . public static Image Load(Configuration configuration, Stream stream, IImageDecoder decoder) where TPixel : unmanaged, IPixel - => WithSeekableStream(configuration, stream, s => decoder.Decode(configuration, s)); + => WithSeekableStream(configuration, stream, s => decoder.Decode(configuration, s, default)); /// /// Create a new instance of the class from the given stream. @@ -511,7 +511,7 @@ namespace SixLabors.ImageSharp => WithSeekableStreamAsync( configuration, stream, - (s, ct) => decoder.DecodeAsync(configuration, s, ct), + (s, ct) => decoder.Decode(configuration, s, ct), cancellationToken); /// @@ -586,7 +586,7 @@ namespace SixLabors.ImageSharp (Image Image, IImageFormat Format) data = await WithSeekableStreamAsync( configuration, stream, - async (s, ct) => await DecodeAsync(s, configuration, ct).ConfigureAwait(false), + (s, ct) => Decode(s, configuration, ct), cancellationToken) .ConfigureAwait(false); @@ -629,7 +629,7 @@ namespace SixLabors.ImageSharp await WithSeekableStreamAsync( configuration, stream, - (s, ct) => DecodeAsync(s, configuration, ct), + (s, ct) => Decode(s, configuration, ct), cancellationToken) .ConfigureAwait(false); @@ -759,7 +759,7 @@ namespace SixLabors.ImageSharp private static async Task WithSeekableStreamAsync( Configuration configuration, Stream stream, - Func> action, + Func action, CancellationToken cancellationToken) { Guard.NotNull(configuration, nameof(configuration)); @@ -770,10 +770,6 @@ namespace SixLabors.ImageSharp throw new NotSupportedException("Cannot read from the stream."); } - // To make sure we don't trigger anything with aspnetcore then we just need to make sure we are - // seekable and we make the copy using CopyToAsync if the stream is seekable then we aren't using - // one of the aspnetcore wrapped streams that error on sync api calls and we can use it without - // having to further wrap if (stream.CanSeek) { if (configuration.ReadOrigin == ReadOrigin.Begin) @@ -781,14 +777,16 @@ namespace SixLabors.ImageSharp stream.Position = 0; } - return await action(stream, cancellationToken).ConfigureAwait(false); + // NOTE: We are explicitly not executing the action against the stream here as we do in WithSeekableStream() because that + // would incur synchronous IO reads which must be avoided in this asynchronous method. Instead, we will *always* run the + // code below to copy the stream to an in-memory buffer before invoking the action. } using var memoryStream = new ChunkedMemoryStream(configuration.MemoryAllocator); await stream.CopyToAsync(memoryStream, configuration.StreamProcessingBufferSize, cancellationToken).ConfigureAwait(false); memoryStream.Position = 0; - return await action(memoryStream, cancellationToken).ConfigureAwait(false); + return action(memoryStream, cancellationToken); } } } diff --git a/src/ImageSharp/ImageFrame{TPixel}.cs b/src/ImageSharp/ImageFrame{TPixel}.cs index fe037003e..716004f39 100644 --- a/src/ImageSharp/ImageFrame{TPixel}.cs +++ b/src/ImageSharp/ImageFrame{TPixel}.cs @@ -362,10 +362,8 @@ namespace SixLabors.ImageSharp return; } - this.PixelBuffer.FastMemoryGroup.TransformTo(destination, (s, d) => - { - PixelOperations.Instance.To(this.GetConfiguration(), s, d); - }); + this.PixelBuffer.FastMemoryGroup.TransformTo(destination, (s, d) + => PixelOperations.Instance.To(this.GetConfiguration(), s, d)); } /// diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj index 1b3a8e27f..7e58607dd 100644 --- a/src/ImageSharp/ImageSharp.csproj +++ b/src/ImageSharp/ImageSharp.csproj @@ -10,35 +10,25 @@ Apache-2.0 https://github.com/SixLabors/ImageSharp/ $(RepositoryUrl) - Image Resize Crop Gif Jpg Jpeg Bitmap Pbm Png Tga NetCore + Image Resize Crop Gif Jpg Jpeg Bitmap Pbm Png Tga Tiff WebP NetCore A new, fully featured, fully managed, cross-platform, 2D graphics API for .NET - Debug;Release;Debug-InnerLoop;Release-InnerLoop + Debug;Release - - 2.0 + + 3.0 - net6.0;net5.0;netcoreapp3.1;netcoreapp2.1;netstandard2.1;netstandard2.0;net472 - - - - - net5.0;netcoreapp3.1;netcoreapp2.1;netstandard2.1;netstandard2.0;net472 - - - - - netcoreapp3.1 + net7.0;net6.0 - netcoreapp3.1;netcoreapp2.1;netstandard2.1;netstandard2.0;net472 + net6.0 @@ -47,23 +37,6 @@ - - - - - - - - - - - - - - - - - True diff --git a/src/ImageSharp/Memory/Allocators/Internals/RefCountedLifetimeGuard.cs b/src/ImageSharp/Memory/Allocators/Internals/RefCountedLifetimeGuard.cs deleted file mode 100644 index 61682aa56..000000000 --- a/src/ImageSharp/Memory/Allocators/Internals/RefCountedLifetimeGuard.cs +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -using System; -using System.Runtime.InteropServices; -using System.Threading; - -namespace SixLabors.ImageSharp.Memory.Internals -{ - /// - /// Implements reference counting lifetime guard mechanism similar to the one provided by , - /// but without the restriction of the guarded object being a handle. - /// - internal abstract class RefCountedLifetimeGuard : IDisposable - { - private int refCount = 1; - private int disposed; - private int released; - - ~RefCountedLifetimeGuard() - { - Interlocked.Exchange(ref this.disposed, 1); - this.ReleaseRef(); - } - - public bool IsDisposed => this.disposed == 1; - - public void AddRef() => Interlocked.Increment(ref this.refCount); - - public void ReleaseRef() - { - Interlocked.Decrement(ref this.refCount); - if (this.refCount == 0) - { - int wasReleased = Interlocked.Exchange(ref this.released, 1); - - if (wasReleased == 0) - { - this.Release(); - } - } - } - - public void Dispose() - { - int wasDisposed = Interlocked.Exchange(ref this.disposed, 1); - if (wasDisposed == 0) - { - this.ReleaseRef(); - GC.SuppressFinalize(this); - } - } - - protected abstract void Release(); - } -} diff --git a/src/ImageSharp/Memory/Allocators/Internals/RefCountedMemoryLifetimeGuard.cs b/src/ImageSharp/Memory/Allocators/Internals/RefCountedMemoryLifetimeGuard.cs new file mode 100644 index 000000000..1c7d6cdfa --- /dev/null +++ b/src/ImageSharp/Memory/Allocators/Internals/RefCountedMemoryLifetimeGuard.cs @@ -0,0 +1,79 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.InteropServices; +using System.Threading; +using SixLabors.ImageSharp.Diagnostics; + +namespace SixLabors.ImageSharp.Memory.Internals +{ + /// + /// Implements reference counting lifetime guard mechanism for memory resources + /// and maintains the value of . + /// + internal abstract class RefCountedMemoryLifetimeGuard : IDisposable + { + private int refCount = 1; + private int disposed; + private int released; + private string allocationStackTrace; + + protected RefCountedMemoryLifetimeGuard() + { + if (MemoryDiagnostics.UndisposedAllocationSubscribed) + { + this.allocationStackTrace = Environment.StackTrace; + } + + MemoryDiagnostics.IncrementTotalUndisposedAllocationCount(); + } + + ~RefCountedMemoryLifetimeGuard() + { + Interlocked.Exchange(ref this.disposed, 1); + this.ReleaseRef(true); + } + + public bool IsDisposed => this.disposed == 1; + + public void AddRef() => Interlocked.Increment(ref this.refCount); + + public void ReleaseRef() => this.ReleaseRef(false); + + public void Dispose() + { + int wasDisposed = Interlocked.Exchange(ref this.disposed, 1); + if (wasDisposed == 0) + { + this.ReleaseRef(); + GC.SuppressFinalize(this); + } + } + + protected abstract void Release(); + + private void ReleaseRef(bool finalizing) + { + Interlocked.Decrement(ref this.refCount); + if (this.refCount == 0) + { + int wasReleased = Interlocked.Exchange(ref this.released, 1); + + if (wasReleased == 0) + { + if (!finalizing) + { + MemoryDiagnostics.DecrementTotalUndisposedAllocationCount(); + } + else if (this.allocationStackTrace != null) + { + MemoryDiagnostics.RaiseUndisposedMemoryResource(this.allocationStackTrace); + } + + this.Release(); + } + } + } + } +} diff --git a/src/ImageSharp/Memory/Allocators/Internals/SharedArrayPoolBuffer{T}.cs b/src/ImageSharp/Memory/Allocators/Internals/SharedArrayPoolBuffer{T}.cs index 9302c67e7..2ea76da95 100644 --- a/src/ImageSharp/Memory/Allocators/Internals/SharedArrayPoolBuffer{T}.cs +++ b/src/ImageSharp/Memory/Allocators/Internals/SharedArrayPoolBuffer{T}.cs @@ -60,7 +60,7 @@ namespace SixLabors.ImageSharp.Memory.Internals } } - private sealed class LifetimeGuard : RefCountedLifetimeGuard + private sealed class LifetimeGuard : RefCountedMemoryLifetimeGuard { private byte[] array; diff --git a/src/ImageSharp/Memory/Allocators/Internals/UniformUnmanagedMemoryPool.LifetimeGuards.cs b/src/ImageSharp/Memory/Allocators/Internals/UniformUnmanagedMemoryPool.LifetimeGuards.cs index 666b24855..151fef69c 100644 --- a/src/ImageSharp/Memory/Allocators/Internals/UniformUnmanagedMemoryPool.LifetimeGuards.cs +++ b/src/ImageSharp/Memory/Allocators/Internals/UniformUnmanagedMemoryPool.LifetimeGuards.cs @@ -20,9 +20,9 @@ namespace SixLabors.ImageSharp.Memory.Internals return buffer; } - public RefCountedLifetimeGuard CreateGroupLifetimeGuard(UnmanagedMemoryHandle[] handles) => new GroupLifetimeGuard(this, handles); + public RefCountedMemoryLifetimeGuard CreateGroupLifetimeGuard(UnmanagedMemoryHandle[] handles) => new GroupLifetimeGuard(this, handles); - private sealed class GroupLifetimeGuard : RefCountedLifetimeGuard + private sealed class GroupLifetimeGuard : RefCountedMemoryLifetimeGuard { private readonly UniformUnmanagedMemoryPool pool; private readonly UnmanagedMemoryHandle[] handles; diff --git a/src/ImageSharp/Memory/Allocators/Internals/UnmanagedBufferLifetimeGuard.cs b/src/ImageSharp/Memory/Allocators/Internals/UnmanagedBufferLifetimeGuard.cs index 5f0759f20..822112024 100644 --- a/src/ImageSharp/Memory/Allocators/Internals/UnmanagedBufferLifetimeGuard.cs +++ b/src/ImageSharp/Memory/Allocators/Internals/UnmanagedBufferLifetimeGuard.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Memory.Internals /// /// Defines a strategy for managing unmanaged memory ownership. /// - internal abstract class UnmanagedBufferLifetimeGuard : RefCountedLifetimeGuard + internal abstract class UnmanagedBufferLifetimeGuard : RefCountedMemoryLifetimeGuard { private UnmanagedMemoryHandle handle; diff --git a/src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs b/src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs index 16a3cb73d..0da4ff9f8 100644 --- a/src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs +++ b/src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs @@ -1,11 +1,10 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. using System; using System.Buffers; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using System.Threading; using SixLabors.ImageSharp.Memory.Internals; namespace SixLabors.ImageSharp.Memory @@ -22,7 +21,7 @@ namespace SixLabors.ImageSharp.Memory private readonly int poolCapacity; private readonly UniformUnmanagedMemoryPool.TrimSettings trimSettings; - private UniformUnmanagedMemoryPool pool; + private readonly UniformUnmanagedMemoryPool pool; private readonly UnmanagedMemoryAllocator nonPoolAllocator; public UniformUnmanagedMemoryPoolMemoryAllocator(int? maxPoolSizeMegabytes) @@ -74,6 +73,12 @@ namespace SixLabors.ImageSharp.Memory this.nonPoolAllocator = new UnmanagedMemoryAllocator(unmanagedBufferSizeInBytes); } +#if NETCOREAPP3_1_OR_GREATER + // This delegate allows overriding the method returning the available system memory, + // so we can test our workaround for https://github.com/dotnet/runtime/issues/65466 + internal static Func GetTotalAvailableMemoryBytes { get; set; } = () => GC.GetGCMemoryInfo().TotalAvailableMemoryBytes; +#endif + /// protected internal override int GetBufferCapacityInBytes() => this.poolBufferSizeInBytes; @@ -152,8 +157,13 @@ namespace SixLabors.ImageSharp.Memory // https://github.com/dotnet/runtime/issues/55126#issuecomment-876779327 if (Environment.Is64BitProcess || !RuntimeInformation.FrameworkDescription.StartsWith(".NET 5.0")) { - GCMemoryInfo info = GC.GetGCMemoryInfo(); - return info.TotalAvailableMemoryBytes / 8; + long total = GetTotalAvailableMemoryBytes(); + + // Workaround for https://github.com/dotnet/runtime/issues/65466 + if (total > 0) + { + return total / 8; + } } #endif diff --git a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs index 21faf8e56..01aac3148 100644 --- a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs +++ b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs @@ -18,7 +18,7 @@ namespace SixLabors.ImageSharp.Memory public sealed class Owned : MemoryGroup, IEnumerable> { private IMemoryOwner[] memoryOwners; - private RefCountedLifetimeGuard groupLifetimeGuard; + private RefCountedMemoryLifetimeGuard groupLifetimeGuard; public Owned(IMemoryOwner[] memoryOwners, int bufferLength, long totalLength, bool swappable) : base(bufferLength, totalLength) diff --git a/src/ImageSharp/Metadata/ImageMetadata.cs b/src/ImageSharp/Metadata/ImageMetadata.cs index 39e1cef8a..4760fa141 100644 --- a/src/ImageSharp/Metadata/ImageMetadata.cs +++ b/src/ImageSharp/Metadata/ImageMetadata.cs @@ -68,11 +68,12 @@ namespace SixLabors.ImageSharp.Metadata this.ExifProfile = other.ExifProfile?.DeepClone(); this.IccProfile = other.IccProfile?.DeepClone(); this.IptcProfile = other.IptcProfile?.DeepClone(); + this.XmpProfile = other.XmpProfile?.DeepClone(); } /// /// Gets or sets the resolution of the image in x- direction. - /// It is defined as the number of dots per inch and should be an positive value. + /// It is defined as the number of dots per and should be an positive value. /// /// The density of the image in x- direction. public double HorizontalResolution @@ -90,7 +91,7 @@ namespace SixLabors.ImageSharp.Metadata /// /// Gets or sets the resolution of the image in y- direction. - /// It is defined as the number of dots per inch and should be an positive value. + /// It is defined as the number of dots per and should be an positive value. /// /// The density of the image in y- direction. public double VerticalResolution @@ -108,10 +109,28 @@ namespace SixLabors.ImageSharp.Metadata /// /// Gets or sets unit of measure used when reporting resolution. - /// 00 : No units; width:height pixel aspect ratio = Ydensity:Xdensity - /// 01 : Pixels per inch (2.54 cm) - /// 02 : Pixels per centimeter - /// 03 : Pixels per meter + /// + /// + /// Value + /// Unit + /// + /// + /// AspectRatio (00) + /// No units; width:height pixel aspect ratio = Ydensity:Xdensity + /// + /// + /// PixelsPerInch (01) + /// Pixels per inch (2.54 cm) + /// + /// + /// PixelsPerCentimeter (02) + /// Pixels per centimeter + /// + /// + /// PixelsPerMeter (03) + /// Pixels per meter (100 cm) + /// + /// /// public PixelResolutionUnit ResolutionUnits { get; set; } @@ -157,7 +176,7 @@ namespace SixLabors.ImageSharp.Metadata } /// - public ImageMetadata DeepClone() => new ImageMetadata(this); + public ImageMetadata DeepClone() => new(this); /// /// Synchronizes the profiles with the current metadata. diff --git a/src/ImageSharp/Metadata/Profiles/Exif/DC-X008-Translation-2019-E.pdf b/src/ImageSharp/Metadata/Profiles/Exif/DC-X008-Translation-2019-E.pdf index cd7141fc8..22a105816 100644 Binary files a/src/ImageSharp/Metadata/Profiles/Exif/DC-X008-Translation-2019-E.pdf and b/src/ImageSharp/Metadata/Profiles/Exif/DC-X008-Translation-2019-E.pdf differ diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifConstants.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifConstants.cs index 0c81f14dd..543e3d5c4 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifConstants.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifConstants.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Text; namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { @@ -22,5 +23,8 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif 0x00, 0x2A }; + + // UTF-8 is better than ASCII, UTF-8 encodes the ASCII codes the same way + public static Encoding DefaultEncoding => Encoding.UTF8; } } diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifEncodedStringHelpers.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifEncodedStringHelpers.cs new file mode 100644 index 000000000..4ec9b3267 --- /dev/null +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifEncodedStringHelpers.cs @@ -0,0 +1,130 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers.Binary; +using System.Text; +using static SixLabors.ImageSharp.Metadata.Profiles.Exif.EncodedString; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif +{ + internal static class ExifEncodedStringHelpers + { + public const int CharacterCodeBytesLength = 8; + + private const ulong AsciiCode = 0x_00_00_00_49_49_43_53_41; + private const ulong JISCode = 0x_00_00_00_00_00_53_49_4A; + private const ulong UnicodeCode = 0x_45_44_4F_43_49_4E_55; + private const ulong UndefinedCode = 0x_00_00_00_00_00_00_00_00; + + private static ReadOnlySpan AsciiCodeBytes => new byte[] { 0x41, 0x53, 0x43, 0x49, 0x49, 0, 0, 0 }; + + private static ReadOnlySpan JISCodeBytes => new byte[] { 0x4A, 0x49, 0x53, 0, 0, 0, 0, 0 }; + + private static ReadOnlySpan UnicodeCodeBytes => new byte[] { 0x55, 0x4E, 0x49, 0x43, 0x4F, 0x44, 0x45, 0 }; + + private static ReadOnlySpan UndefinedCodeBytes => new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 }; + + // 20932 EUC-JP Japanese (JIS 0208-1990 and 0212-1990) + // https://docs.microsoft.com/en-us/dotnet/api/system.text.encoding?view=net-6.0 + private static Encoding JIS0208Encoding => CodePagesEncodingProvider.Instance.GetEncoding(20932); + + public static bool IsEncodedString(ExifTagValue tag) => tag switch + { + ExifTagValue.UserComment or ExifTagValue.GPSProcessingMethod or ExifTagValue.GPSAreaInformation => true, + _ => false + }; + + public static ReadOnlySpan GetCodeBytes(CharacterCode code) => code switch + { + CharacterCode.ASCII => AsciiCodeBytes, + CharacterCode.JIS => JISCodeBytes, + CharacterCode.Unicode => UnicodeCodeBytes, + CharacterCode.Undefined => UndefinedCodeBytes, + _ => UndefinedCodeBytes + }; + + public static Encoding GetEncoding(CharacterCode code) => code switch + { + CharacterCode.ASCII => Encoding.ASCII, + CharacterCode.JIS => JIS0208Encoding, + CharacterCode.Unicode => Encoding.Unicode, + CharacterCode.Undefined => Encoding.UTF8, + _ => Encoding.UTF8 + }; + + public static bool TryParse(ReadOnlySpan buffer, out EncodedString encodedString) + { + if (TryDetect(buffer, out CharacterCode code)) + { + string text = GetEncoding(code).GetString(buffer.Slice(CharacterCodeBytesLength)); + encodedString = new EncodedString(code, text); + return true; + } + + encodedString = default; + return false; + } + + public static uint GetDataLength(EncodedString encodedString) => + (uint)GetEncoding(encodedString.Code).GetByteCount(encodedString.Text) + CharacterCodeBytesLength; + + public static int Write(EncodedString encodedString, Span destination) + { + GetCodeBytes(encodedString.Code).CopyTo(destination); + + string text = encodedString.Text; + int count = Write(GetEncoding(encodedString.Code), text, destination.Slice(CharacterCodeBytesLength)); + + return CharacterCodeBytesLength + count; + } + + public static unsafe int Write(Encoding encoding, string value, Span destination) +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER || NET + => encoding.GetBytes(value.AsSpan(), destination); +#else + { + if (value.Length == 0) + { + return 0; + } + + fixed (char* c = value) + { + fixed (byte* b = destination) + { + return encoding.GetBytes(c, value.Length, b, destination.Length); + } + } + } +#endif + + private static bool TryDetect(ReadOnlySpan buffer, out CharacterCode code) + { + if (buffer.Length >= CharacterCodeBytesLength) + { + ulong test = BinaryPrimitives.ReadUInt64LittleEndian(buffer); + switch (test) + { + case AsciiCode: + code = CharacterCode.ASCII; + return true; + case JISCode: + code = CharacterCode.JIS; + return true; + case UnicodeCode: + code = CharacterCode.Unicode; + return true; + case UndefinedCode: + code = CharacterCode.Undefined; + return true; + default: + break; + } + } + + code = default; + return false; + } + } +} diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs index e0ab8ecf9..665e4753a 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs @@ -121,6 +121,14 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif } } + /// + /// Returns the thumbnail in the EXIF profile when available. + /// + /// + /// The . + /// + public Image CreateThumbnail() => this.CreateThumbnail(); + /// /// Returns the thumbnail in the EXIF profile when available. /// diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs index 2fcd1cc07..abfe835b1 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs @@ -202,7 +202,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif protected void ReadValues64(List values, ulong offset) { - DebugGuard.MustBeLessThanOrEqualTo(offset, (ulong)this.data.Length, "By spec UInt64.MaxValue is supported, but .Net Stream.Length can Int64.MaxValue."); + DebugGuard.MustBeLessThanOrEqualTo(offset, (ulong)this.data.Length, "By spec UInt64.MaxValue is supported, but .NET Stream.Length can Int64.MaxValue."); this.Seek(offset); ulong count = this.ReadUInt64(); @@ -241,9 +241,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif return result; } - private byte ConvertToByte(ReadOnlySpan buffer) => buffer[0]; - - private string ConvertToString(ReadOnlySpan buffer) + private static string ConvertToString(Encoding encoding, ReadOnlySpan buffer) { int nullCharIndex = buffer.IndexOf((byte)0); @@ -252,9 +250,11 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif buffer = buffer.Slice(0, nullCharIndex); } - return Encoding.UTF8.GetString(buffer); + return encoding.GetString(buffer); } + private byte ConvertToByte(ReadOnlySpan buffer) => buffer[0]; + private object ConvertValue(ExifDataType dataType, ReadOnlySpan buffer, bool isArray) { if (buffer.Length == 0) @@ -267,8 +267,9 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif case ExifDataType.Unknown: return null; case ExifDataType.Ascii: - return this.ConvertToString(buffer); + return ConvertToString(ExifConstants.DefaultEncoding, buffer); case ExifDataType.Byte: + case ExifDataType.Undefined: if (!isArray) { return this.ConvertToByte(buffer); @@ -354,13 +355,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif } return ToArray(dataType, buffer, this.ConvertToUInt64); - case ExifDataType.Undefined: - if (!isArray) - { - return this.ConvertToByte(buffer); - } - return buffer.ToArray(); default: throw new NotSupportedException($"Data type {dataType} is not supported."); } @@ -453,7 +448,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif // Likewise, tags that point to other IFDs, like e.g. the SubIFDs tag, are now allowed to have the datatype TIFF_IFD8 in BigTIFF. // Again, the old datatypes TIFF_IFD, and the hardly recommendable TIFF_LONG, are still valid, too. // https://www.awaresystems.be/imaging/tiff/bigtiff.html - ExifValue exifValue = null; + ExifValue exifValue; switch (tag) { case ExifTagValue.StripOffsets: diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifUcs2StringHelpers.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifUcs2StringHelpers.cs new file mode 100644 index 000000000..ccc1c80ad --- /dev/null +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifUcs2StringHelpers.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Text; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif +{ + internal static class ExifUcs2StringHelpers + { + public static Encoding Ucs2Encoding => Encoding.GetEncoding("UCS-2"); + + public static bool IsUcs2Tag(ExifTagValue tag) => tag switch + { + ExifTagValue.XPAuthor or ExifTagValue.XPComment or ExifTagValue.XPKeywords or ExifTagValue.XPSubject or ExifTagValue.XPTitle => true, + _ => false, + }; + + public static int Write(string value, Span destination) => ExifEncodedStringHelpers.Write(Ucs2Encoding, value, destination); + } +} diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs index e2ed56954..a14539bca 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs @@ -4,7 +4,6 @@ using System; using System.Buffers.Binary; using System.Collections.Generic; -using System.Text; namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { @@ -274,9 +273,19 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { object value = exifValue.GetValue(); + if (ExifUcs2StringHelpers.IsUcs2Tag((ExifTagValue)(ushort)exifValue.Tag)) + { + return (uint)ExifUcs2StringHelpers.Ucs2Encoding.GetByteCount((string)value); + } + + if (value is EncodedString encodedString) + { + return ExifEncodedStringHelpers.GetDataLength(encodedString); + } + if (exifValue.DataType == ExifDataType.Ascii) { - return (uint)Encoding.UTF8.GetBytes((string)value).Length + 1; + return (uint)ExifConstants.DefaultEncoding.GetByteCount((string)value) + 1; } if (value is Array arrayValue) @@ -289,11 +298,6 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif private static int WriteArray(IExifValue value, Span destination, int offset) { - if (value.DataType == ExifDataType.Ascii) - { - return WriteValue(ExifDataType.Ascii, value.GetValue(), destination, offset); - } - int newOffset = offset; foreach (object obj in (Array)value.GetValue()) { @@ -378,7 +382,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif switch (dataType) { case ExifDataType.Ascii: - offset = Write(Encoding.UTF8.GetBytes((string)value), destination, offset); + offset = Write(ExifConstants.DefaultEncoding.GetBytes((string)value), destination, offset); destination[offset] = 0; return offset + 1; case ExifDataType.Byte: @@ -425,14 +429,25 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif } } - internal static int WriteValue(IExifValue value, Span destination, int offset) + internal static int WriteValue(IExifValue exifValue, Span destination, int offset) { - if (value.IsArray && value.DataType != ExifDataType.Ascii) + object value = exifValue.GetValue(); + + if (ExifUcs2StringHelpers.IsUcs2Tag((ExifTagValue)(ushort)exifValue.Tag)) + { + return offset + ExifUcs2StringHelpers.Write((string)value, destination.Slice(offset)); + } + else if (value is EncodedString encodedString) + { + return offset + ExifEncodedStringHelpers.Write(encodedString, destination.Slice(offset)); + } + + if (exifValue.IsArray) { - return WriteArray(value, destination, offset); + return WriteArray(exifValue, destination, offset); } - return WriteValue(value.DataType, value.GetValue(), destination, offset); + return WriteValue(exifValue.DataType, value, destination, offset); } } } diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.ByteArray.cs b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.ByteArray.cs index fdde66c51..964fb6e94 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.ByteArray.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.ByteArray.cs @@ -41,31 +41,6 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif /// public static ExifTag TIFFEPStandardID => new ExifTag(ExifTagValue.TIFFEPStandardID); - /// - /// Gets the XPTitle exif tag. - /// - public static ExifTag XPTitle => new ExifTag(ExifTagValue.XPTitle); - - /// - /// Gets the XPComment exif tag. - /// - public static ExifTag XPComment => new ExifTag(ExifTagValue.XPComment); - - /// - /// Gets the XPAuthor exif tag. - /// - public static ExifTag XPAuthor => new ExifTag(ExifTagValue.XPAuthor); - - /// - /// Gets the XPKeywords exif tag. - /// - public static ExifTag XPKeywords => new ExifTag(ExifTagValue.XPKeywords); - - /// - /// Gets the XPSubject exif tag. - /// - public static ExifTag XPSubject => new ExifTag(ExifTagValue.XPSubject); - /// /// Gets the GPSVersionID exif tag. /// diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.EncodedString.cs b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.EncodedString.cs new file mode 100644 index 000000000..335098a43 --- /dev/null +++ b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.EncodedString.cs @@ -0,0 +1,24 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif +{ + /// + public abstract partial class ExifTag + { + /// + /// Gets the UserComment exif tag. + /// + public static ExifTag UserComment { get; } = new ExifTag(ExifTagValue.UserComment); + + /// + /// Gets the GPSProcessingMethod exif tag. + /// + public static ExifTag GPSProcessingMethod { get; } = new ExifTag(ExifTagValue.GPSProcessingMethod); + + /// + /// Gets the GPSAreaInformation exif tag. + /// + public static ExifTag GPSAreaInformation { get; } = new ExifTag(ExifTagValue.GPSAreaInformation); + } +} diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.String.cs b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.String.cs index 3fc353211..286b31786 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.String.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.String.cs @@ -94,7 +94,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif /// /// Gets the MDFileUnits exif tag. /// - public static ExifTag MDFileUnits => new ExifTag(ExifTagValue.MDFileUnits); + public static ExifTag MDFileUnits { get; } = new ExifTag(ExifTagValue.MDFileUnits); /// /// Gets the SEMInfo exif tag. diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Ucs2String.cs b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Ucs2String.cs new file mode 100644 index 000000000..a6911d76d --- /dev/null +++ b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Ucs2String.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif +{ + /// + public abstract partial class ExifTag + { + /// + /// Gets the title tag used by Windows (encoded in UCS2). + /// + public static ExifTag XPTitle => new ExifTag(ExifTagValue.XPTitle); + + /// + /// Gets the comment tag used by Windows (encoded in UCS2). + /// + public static ExifTag XPComment => new ExifTag(ExifTagValue.XPComment); + + /// + /// Gets the author tag used by Windows (encoded in UCS2). + /// + public static ExifTag XPAuthor => new ExifTag(ExifTagValue.XPAuthor); + + /// + /// Gets the keywords tag used by Windows (encoded in UCS2). + /// + public static ExifTag XPKeywords => new ExifTag(ExifTagValue.XPKeywords); + + /// + /// Gets the subject tag used by Windows (encoded in UCS2). + /// + public static ExifTag XPSubject => new ExifTag(ExifTagValue.XPSubject); + } +} diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Undefined.cs b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Undefined.cs index 1d9af6adc..58886f403 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Undefined.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Undefined.cs @@ -31,11 +31,6 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif /// public static ExifTag MakerNote { get; } = new ExifTag(ExifTagValue.MakerNote); - /// - /// Gets the UserComment exif tag. - /// - public static ExifTag UserComment { get; } = new ExifTag(ExifTagValue.UserComment); - /// /// Gets the FlashpixVersion exif tag. /// @@ -71,16 +66,6 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif /// public static ExifTag ImageSourceData { get; } = new ExifTag(ExifTagValue.ImageSourceData); - /// - /// Gets the GPSProcessingMethod exif tag. - /// - public static ExifTag GPSProcessingMethod { get; } = new ExifTag(ExifTagValue.GPSProcessingMethod); - - /// - /// Gets the GPSAreaInformation exif tag. - /// - public static ExifTag GPSAreaInformation { get; } = new ExifTag(ExifTagValue.GPSAreaInformation); - /// /// Gets the FileSource exif tag. /// diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Values/EncodedString.cs b/src/ImageSharp/Metadata/Profiles/Exif/Values/EncodedString.cs new file mode 100644 index 000000000..e9cd27427 --- /dev/null +++ b/src/ImageSharp/Metadata/Profiles/Exif/Values/EncodedString.cs @@ -0,0 +1,94 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif +{ + /// + /// The EXIF encoded string structure. + /// + public readonly struct EncodedString : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// Default use Unicode character code. + /// + /// The text value. + public EncodedString(string text) + : this(CharacterCode.Unicode, text) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The character code. + /// The text value. + public EncodedString(CharacterCode code, string text) + { + this.Text = text; + this.Code = code; + } + + /// + /// The 8-byte character code enum. + /// + public enum CharacterCode + { + /// + /// The ASCII (ITU-T T.50 IA5) character code. + /// + ASCII, + + /// + /// The JIS (X208-1990) character code. + /// + JIS, + + /// + /// The Unicode character code. + /// + Unicode, + + /// + /// The undefined character code. + /// + Undefined + } + + /// + /// Gets the character ode. + /// + public CharacterCode Code { get; } + + /// + /// Gets the text. + /// + public string Text { get; } + + /// + /// Converts the specified to an instance of this type. + /// + /// The text value. + public static implicit operator EncodedString(string text) => new(text); + + /// + /// Converts the specified to a . + /// + /// The to convert. + public static explicit operator string(EncodedString encodedString) => encodedString.Text; + + /// + public override bool Equals(object obj) => obj is EncodedString other && this.Equals(other); + + /// + public bool Equals(EncodedString other) => this.Text == other.Text && this.Code == other.Code; + + /// + public override int GetHashCode() => HashCode.Combine(this.Text, this.Code); + + /// + public override string ToString() => this.Text; + } +} diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifEncodedString.cs b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifEncodedString.cs new file mode 100644 index 000000000..ba9fca5c8 --- /dev/null +++ b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifEncodedString.cs @@ -0,0 +1,55 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Globalization; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif +{ + internal sealed class ExifEncodedString : ExifValue + { + public ExifEncodedString(ExifTag tag) + : base(tag) + { + } + + public ExifEncodedString(ExifTagValue tag) + : base(tag) + { + } + + private ExifEncodedString(ExifEncodedString value) + : base(value) + { + } + + public override ExifDataType DataType => ExifDataType.Undefined; + + protected override string StringValue => this.Value.Text; + + public override bool TrySetValue(object value) + { + if (base.TrySetValue(value)) + { + return true; + } + + if (value is string stringValue) + { + this.Value = new EncodedString(stringValue); + return true; + } + else if (value is byte[] buffer) + { + if (ExifEncodedStringHelpers.TryParse(buffer, out EncodedString encodedString)) + { + this.Value = encodedString; + return true; + } + } + + return false; + } + + public override IExifValue DeepClone() => new ExifEncodedString(this); + } +} diff --git a/src/ImageSharp/Processing/OrientationMode.cs b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifOrientationMode.cs similarity index 69% rename from src/ImageSharp/Processing/OrientationMode.cs rename to src/ImageSharp/Metadata/Profiles/Exif/Values/ExifOrientationMode.cs index a8ba5a55c..b68390ae0 100644 --- a/src/ImageSharp/Processing/OrientationMode.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifOrientationMode.cs @@ -1,56 +1,56 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. -namespace SixLabors.ImageSharp.Processing +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { /// /// Enumerates the available orientation values supplied by EXIF metadata. /// - internal enum OrientationMode : ushort + public static class ExifOrientationMode { /// /// Unknown rotation. /// - Unknown = 0, + public const ushort Unknown = 0; /// /// The 0th row at the top, the 0th column on the left. /// - TopLeft = 1, + public const ushort TopLeft = 1; /// /// The 0th row at the top, the 0th column on the right. /// - TopRight = 2, + public const ushort TopRight = 2; /// /// The 0th row at the bottom, the 0th column on the right. /// - BottomRight = 3, + public const ushort BottomRight = 3; /// /// The 0th row at the bottom, the 0th column on the left. /// - BottomLeft = 4, + public const ushort BottomLeft = 4; /// /// The 0th row on the left, the 0th column at the top. /// - LeftTop = 5, + public const ushort LeftTop = 5; /// /// The 0th row at the right, the 0th column at the top. /// - RightTop = 6, + public const ushort RightTop = 6; /// /// The 0th row on the right, the 0th column at the bottom. /// - RightBottom = 7, + public const ushort RightBottom = 7; /// /// The 0th row on the left, the 0th column at the bottom. /// - LeftBottom = 8 + public const ushort LeftBottom = 8; } } diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifUcs2String.cs b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifUcs2String.cs new file mode 100644 index 000000000..42637925c --- /dev/null +++ b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifUcs2String.cs @@ -0,0 +1,47 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif +{ + internal sealed class ExifUcs2String : ExifValue + { + public ExifUcs2String(ExifTag tag) + : base(tag) + { + } + + public ExifUcs2String(ExifTagValue tag) + : base(tag) + { + } + + private ExifUcs2String(ExifUcs2String value) + : base(value) + { + } + + public override ExifDataType DataType => ExifDataType.Byte; + + protected override string StringValue => this.Value; + + public override object GetValue() => this.Value; + + public override bool TrySetValue(object value) + { + if (base.TrySetValue(value)) + { + return true; + } + + if (value is byte[] buffer) + { + this.Value = ExifUcs2StringHelpers.Ucs2Encoding.GetString(buffer); + return true; + } + + return false; + } + + public override IExifValue DeepClone() => new ExifUcs2String(this); + } +} diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValue.cs b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValue.cs index 314333919..82bd6ad2e 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValue.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValue.cs @@ -29,7 +29,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { // All array types are value types so Clone() is sufficient here. var array = (Array)other.GetValue(); - this.TrySetValue(array.Clone()); + this.TrySetValue(array?.Clone()); } } diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs index 33fb90cc0..fa5cf9b2f 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs @@ -15,21 +15,36 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { switch (dataType) { - case ExifDataType.Byte: return isArray ? (ExifValue)new ExifByteArray(tag, dataType) : new ExifByte(tag, dataType); - case ExifDataType.DoubleFloat: return isArray ? (ExifValue)new ExifDoubleArray(tag) : new ExifDouble(tag); - case ExifDataType.SingleFloat: return isArray ? (ExifValue)new ExifFloatArray(tag) : new ExifFloat(tag); - case ExifDataType.Long: return isArray ? (ExifValue)new ExifLongArray(tag) : new ExifLong(tag); - case ExifDataType.Long8: return isArray ? (ExifValue)new ExifLong8Array(tag) : new ExifLong8(tag); - case ExifDataType.Rational: return isArray ? (ExifValue)new ExifRationalArray(tag) : new ExifRational(tag); - case ExifDataType.Short: return isArray ? (ExifValue)new ExifShortArray(tag) : new ExifShort(tag); - case ExifDataType.SignedByte: return isArray ? (ExifValue)new ExifSignedByteArray(tag) : new ExifSignedByte(tag); - case ExifDataType.SignedLong: return isArray ? (ExifValue)new ExifSignedLongArray(tag) : new ExifSignedLong(tag); - case ExifDataType.SignedLong8: return isArray ? (ExifValue)new ExifSignedLong8Array(tag) : new ExifSignedLong8(tag); - case ExifDataType.SignedRational: return isArray ? (ExifValue)new ExifSignedRationalArray(tag) : new ExifSignedRational(tag); - case ExifDataType.SignedShort: return isArray ? (ExifValue)new ExifSignedShortArray(tag) : new ExifSignedShort(tag); - case ExifDataType.Ascii: return new ExifString(tag); - case ExifDataType.Undefined: return isArray ? (ExifValue)new ExifByteArray(tag, dataType) : new ExifByte(tag, dataType); - default: return null; + case ExifDataType.Byte: + return isArray ? new ExifByteArray(tag, dataType) : new ExifByte(tag, dataType); + case ExifDataType.DoubleFloat: + return isArray ? new ExifDoubleArray(tag) : new ExifDouble(tag); + case ExifDataType.SingleFloat: + return isArray ? new ExifFloatArray(tag) : new ExifFloat(tag); + case ExifDataType.Long: + return isArray ? new ExifLongArray(tag) : new ExifLong(tag); + case ExifDataType.Long8: + return isArray ? new ExifLong8Array(tag) : new ExifLong8(tag); + case ExifDataType.Rational: + return isArray ? new ExifRationalArray(tag) : new ExifRational(tag); + case ExifDataType.Short: + return isArray ? new ExifShortArray(tag) : new ExifShort(tag); + case ExifDataType.SignedByte: + return isArray ? new ExifSignedByteArray(tag) : new ExifSignedByte(tag); + case ExifDataType.SignedLong: + return isArray ? new ExifSignedLongArray(tag) : new ExifSignedLong(tag); + case ExifDataType.SignedLong8: + return isArray ? new ExifSignedLong8Array(tag) : new ExifSignedLong8(tag); + case ExifDataType.SignedRational: + return isArray ? new ExifSignedRationalArray(tag) : new ExifSignedRational(tag); + case ExifDataType.SignedShort: + return isArray ? new ExifSignedShortArray(tag) : new ExifSignedShort(tag); + case ExifDataType.Ascii: + return new ExifString(tag); + case ExifDataType.Undefined: + return isArray ? new ExifByteArray(tag, dataType) : new ExifByte(tag, dataType); + default: + return null; } } @@ -37,275 +52,530 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { switch (tag) { - case ExifTagValue.FaxProfile: return new ExifByte(ExifTag.FaxProfile, ExifDataType.Byte); - case ExifTagValue.ModeNumber: return new ExifByte(ExifTag.ModeNumber, ExifDataType.Byte); - case ExifTagValue.GPSAltitudeRef: return new ExifByte(ExifTag.GPSAltitudeRef, ExifDataType.Byte); + case ExifTagValue.FaxProfile: + return new ExifByte(ExifTag.FaxProfile, ExifDataType.Byte); + case ExifTagValue.ModeNumber: + return new ExifByte(ExifTag.ModeNumber, ExifDataType.Byte); + case ExifTagValue.GPSAltitudeRef: + return new ExifByte(ExifTag.GPSAltitudeRef, ExifDataType.Byte); - case ExifTagValue.ClipPath: return new ExifByteArray(ExifTag.ClipPath, ExifDataType.Byte); - case ExifTagValue.VersionYear: return new ExifByteArray(ExifTag.VersionYear, ExifDataType.Byte); - case ExifTagValue.XMP: return new ExifByteArray(ExifTag.XMP, ExifDataType.Byte); - case ExifTagValue.CFAPattern2: return new ExifByteArray(ExifTag.CFAPattern2, ExifDataType.Byte); - case ExifTagValue.TIFFEPStandardID: return new ExifByteArray(ExifTag.TIFFEPStandardID, ExifDataType.Byte); - case ExifTagValue.XPTitle: return new ExifByteArray(ExifTag.XPTitle, ExifDataType.Byte); - case ExifTagValue.XPComment: return new ExifByteArray(ExifTag.XPComment, ExifDataType.Byte); - case ExifTagValue.XPAuthor: return new ExifByteArray(ExifTag.XPAuthor, ExifDataType.Byte); - case ExifTagValue.XPKeywords: return new ExifByteArray(ExifTag.XPKeywords, ExifDataType.Byte); - case ExifTagValue.XPSubject: return new ExifByteArray(ExifTag.XPSubject, ExifDataType.Byte); - case ExifTagValue.GPSVersionID: return new ExifByteArray(ExifTag.GPSVersionID, ExifDataType.Byte); + case ExifTagValue.ClipPath: + return new ExifByteArray(ExifTag.ClipPath, ExifDataType.Byte); + case ExifTagValue.VersionYear: + return new ExifByteArray(ExifTag.VersionYear, ExifDataType.Byte); + case ExifTagValue.XMP: + return new ExifByteArray(ExifTag.XMP, ExifDataType.Byte); + case ExifTagValue.CFAPattern2: + return new ExifByteArray(ExifTag.CFAPattern2, ExifDataType.Byte); + case ExifTagValue.TIFFEPStandardID: + return new ExifByteArray(ExifTag.TIFFEPStandardID, ExifDataType.Byte); + case ExifTagValue.GPSVersionID: + return new ExifByteArray(ExifTag.GPSVersionID, ExifDataType.Byte); - case ExifTagValue.PixelScale: return new ExifDoubleArray(ExifTag.PixelScale); - case ExifTagValue.IntergraphMatrix: return new ExifDoubleArray(ExifTag.IntergraphMatrix); - case ExifTagValue.ModelTiePoint: return new ExifDoubleArray(ExifTag.ModelTiePoint); - case ExifTagValue.ModelTransform: return new ExifDoubleArray(ExifTag.ModelTransform); + case ExifTagValue.PixelScale: + return new ExifDoubleArray(ExifTag.PixelScale); + case ExifTagValue.IntergraphMatrix: + return new ExifDoubleArray(ExifTag.IntergraphMatrix); + case ExifTagValue.ModelTiePoint: + return new ExifDoubleArray(ExifTag.ModelTiePoint); + case ExifTagValue.ModelTransform: + return new ExifDoubleArray(ExifTag.ModelTransform); - case ExifTagValue.SubfileType: return new ExifLong(ExifTag.SubfileType); - case ExifTagValue.SubIFDOffset: return new ExifLong(ExifTag.SubIFDOffset); - case ExifTagValue.GPSIFDOffset: return new ExifLong(ExifTag.GPSIFDOffset); - case ExifTagValue.T4Options: return new ExifLong(ExifTag.T4Options); - case ExifTagValue.T6Options: return new ExifLong(ExifTag.T6Options); - case ExifTagValue.XClipPathUnits: return new ExifLong(ExifTag.XClipPathUnits); - case ExifTagValue.YClipPathUnits: return new ExifLong(ExifTag.YClipPathUnits); - case ExifTagValue.ProfileType: return new ExifLong(ExifTag.ProfileType); - case ExifTagValue.CodingMethods: return new ExifLong(ExifTag.CodingMethods); - case ExifTagValue.T82ptions: return new ExifLong(ExifTag.T82ptions); - case ExifTagValue.JPEGInterchangeFormat: return new ExifLong(ExifTag.JPEGInterchangeFormat); - case ExifTagValue.JPEGInterchangeFormatLength: return new ExifLong(ExifTag.JPEGInterchangeFormatLength); - case ExifTagValue.MDFileTag: return new ExifLong(ExifTag.MDFileTag); - case ExifTagValue.StandardOutputSensitivity: return new ExifLong(ExifTag.StandardOutputSensitivity); - case ExifTagValue.RecommendedExposureIndex: return new ExifLong(ExifTag.RecommendedExposureIndex); - case ExifTagValue.ISOSpeed: return new ExifLong(ExifTag.ISOSpeed); - case ExifTagValue.ISOSpeedLatitudeyyy: return new ExifLong(ExifTag.ISOSpeedLatitudeyyy); - case ExifTagValue.ISOSpeedLatitudezzz: return new ExifLong(ExifTag.ISOSpeedLatitudezzz); - case ExifTagValue.FaxRecvParams: return new ExifLong(ExifTag.FaxRecvParams); - case ExifTagValue.FaxRecvTime: return new ExifLong(ExifTag.FaxRecvTime); - case ExifTagValue.ImageNumber: return new ExifLong(ExifTag.ImageNumber); + case ExifTagValue.SubfileType: + return new ExifLong(ExifTag.SubfileType); + case ExifTagValue.SubIFDOffset: + return new ExifLong(ExifTag.SubIFDOffset); + case ExifTagValue.GPSIFDOffset: + return new ExifLong(ExifTag.GPSIFDOffset); + case ExifTagValue.T4Options: + return new ExifLong(ExifTag.T4Options); + case ExifTagValue.T6Options: + return new ExifLong(ExifTag.T6Options); + case ExifTagValue.XClipPathUnits: + return new ExifLong(ExifTag.XClipPathUnits); + case ExifTagValue.YClipPathUnits: + return new ExifLong(ExifTag.YClipPathUnits); + case ExifTagValue.ProfileType: + return new ExifLong(ExifTag.ProfileType); + case ExifTagValue.CodingMethods: + return new ExifLong(ExifTag.CodingMethods); + case ExifTagValue.T82ptions: + return new ExifLong(ExifTag.T82ptions); + case ExifTagValue.JPEGInterchangeFormat: + return new ExifLong(ExifTag.JPEGInterchangeFormat); + case ExifTagValue.JPEGInterchangeFormatLength: + return new ExifLong(ExifTag.JPEGInterchangeFormatLength); + case ExifTagValue.MDFileTag: + return new ExifLong(ExifTag.MDFileTag); + case ExifTagValue.StandardOutputSensitivity: + return new ExifLong(ExifTag.StandardOutputSensitivity); + case ExifTagValue.RecommendedExposureIndex: + return new ExifLong(ExifTag.RecommendedExposureIndex); + case ExifTagValue.ISOSpeed: + return new ExifLong(ExifTag.ISOSpeed); + case ExifTagValue.ISOSpeedLatitudeyyy: + return new ExifLong(ExifTag.ISOSpeedLatitudeyyy); + case ExifTagValue.ISOSpeedLatitudezzz: + return new ExifLong(ExifTag.ISOSpeedLatitudezzz); + case ExifTagValue.FaxRecvParams: + return new ExifLong(ExifTag.FaxRecvParams); + case ExifTagValue.FaxRecvTime: + return new ExifLong(ExifTag.FaxRecvTime); + case ExifTagValue.ImageNumber: + return new ExifLong(ExifTag.ImageNumber); - case ExifTagValue.FreeOffsets: return new ExifLongArray(ExifTag.FreeOffsets); - case ExifTagValue.FreeByteCounts: return new ExifLongArray(ExifTag.FreeByteCounts); - case ExifTagValue.ColorResponseUnit: return new ExifLongArray(ExifTag.ColorResponseUnit); - case ExifTagValue.TileOffsets: return new ExifLongArray(ExifTag.TileOffsets); - case ExifTagValue.SMinSampleValue: return new ExifLongArray(ExifTag.SMinSampleValue); - case ExifTagValue.SMaxSampleValue: return new ExifLongArray(ExifTag.SMaxSampleValue); - case ExifTagValue.JPEGQTables: return new ExifLongArray(ExifTag.JPEGQTables); - case ExifTagValue.JPEGDCTables: return new ExifLongArray(ExifTag.JPEGDCTables); - case ExifTagValue.JPEGACTables: return new ExifLongArray(ExifTag.JPEGACTables); - case ExifTagValue.StripRowCounts: return new ExifLongArray(ExifTag.StripRowCounts); - case ExifTagValue.IntergraphRegisters: return new ExifLongArray(ExifTag.IntergraphRegisters); - case ExifTagValue.TimeZoneOffset: return new ExifLongArray(ExifTag.TimeZoneOffset); - case ExifTagValue.SubIFDs: return new ExifLongArray(ExifTag.SubIFDs); + case ExifTagValue.FreeOffsets: + return new ExifLongArray(ExifTag.FreeOffsets); + case ExifTagValue.FreeByteCounts: + return new ExifLongArray(ExifTag.FreeByteCounts); + case ExifTagValue.ColorResponseUnit: + return new ExifLongArray(ExifTag.ColorResponseUnit); + case ExifTagValue.TileOffsets: + return new ExifLongArray(ExifTag.TileOffsets); + case ExifTagValue.SMinSampleValue: + return new ExifLongArray(ExifTag.SMinSampleValue); + case ExifTagValue.SMaxSampleValue: + return new ExifLongArray(ExifTag.SMaxSampleValue); + case ExifTagValue.JPEGQTables: + return new ExifLongArray(ExifTag.JPEGQTables); + case ExifTagValue.JPEGDCTables: + return new ExifLongArray(ExifTag.JPEGDCTables); + case ExifTagValue.JPEGACTables: + return new ExifLongArray(ExifTag.JPEGACTables); + case ExifTagValue.StripRowCounts: + return new ExifLongArray(ExifTag.StripRowCounts); + case ExifTagValue.IntergraphRegisters: + return new ExifLongArray(ExifTag.IntergraphRegisters); + case ExifTagValue.TimeZoneOffset: + return new ExifLongArray(ExifTag.TimeZoneOffset); + case ExifTagValue.SubIFDs: + return new ExifLongArray(ExifTag.SubIFDs); - case ExifTagValue.ImageWidth: return new ExifNumber(ExifTag.ImageWidth); - case ExifTagValue.ImageLength: return new ExifNumber(ExifTag.ImageLength); - case ExifTagValue.RowsPerStrip: return new ExifNumber(ExifTag.RowsPerStrip); - case ExifTagValue.TileWidth: return new ExifNumber(ExifTag.TileWidth); - case ExifTagValue.TileLength: return new ExifNumber(ExifTag.TileLength); - case ExifTagValue.BadFaxLines: return new ExifNumber(ExifTag.BadFaxLines); - case ExifTagValue.ConsecutiveBadFaxLines: return new ExifNumber(ExifTag.ConsecutiveBadFaxLines); - case ExifTagValue.PixelXDimension: return new ExifNumber(ExifTag.PixelXDimension); - case ExifTagValue.PixelYDimension: return new ExifNumber(ExifTag.PixelYDimension); + case ExifTagValue.ImageWidth: + return new ExifNumber(ExifTag.ImageWidth); + case ExifTagValue.ImageLength: + return new ExifNumber(ExifTag.ImageLength); + case ExifTagValue.RowsPerStrip: + return new ExifNumber(ExifTag.RowsPerStrip); + case ExifTagValue.TileWidth: + return new ExifNumber(ExifTag.TileWidth); + case ExifTagValue.TileLength: + return new ExifNumber(ExifTag.TileLength); + case ExifTagValue.BadFaxLines: + return new ExifNumber(ExifTag.BadFaxLines); + case ExifTagValue.ConsecutiveBadFaxLines: + return new ExifNumber(ExifTag.ConsecutiveBadFaxLines); + case ExifTagValue.PixelXDimension: + return new ExifNumber(ExifTag.PixelXDimension); + case ExifTagValue.PixelYDimension: + return new ExifNumber(ExifTag.PixelYDimension); - case ExifTagValue.StripByteCounts: return new ExifNumberArray(ExifTag.StripByteCounts); - case ExifTagValue.StripOffsets: return new ExifNumberArray(ExifTag.StripOffsets); - case ExifTagValue.TileByteCounts: return new ExifNumberArray(ExifTag.TileByteCounts); - case ExifTagValue.ImageLayer: return new ExifNumberArray(ExifTag.ImageLayer); + case ExifTagValue.StripByteCounts: + return new ExifNumberArray(ExifTag.StripByteCounts); + case ExifTagValue.StripOffsets: + return new ExifNumberArray(ExifTag.StripOffsets); + case ExifTagValue.TileByteCounts: + return new ExifNumberArray(ExifTag.TileByteCounts); + case ExifTagValue.ImageLayer: + return new ExifNumberArray(ExifTag.ImageLayer); - case ExifTagValue.XPosition: return new ExifRational(ExifTag.XPosition); - case ExifTagValue.YPosition: return new ExifRational(ExifTag.YPosition); - case ExifTagValue.XResolution: return new ExifRational(ExifTag.XResolution); - case ExifTagValue.YResolution: return new ExifRational(ExifTag.YResolution); - case ExifTagValue.BatteryLevel: return new ExifRational(ExifTag.BatteryLevel); - case ExifTagValue.ExposureTime: return new ExifRational(ExifTag.ExposureTime); - case ExifTagValue.FNumber: return new ExifRational(ExifTag.FNumber); - case ExifTagValue.MDScalePixel: return new ExifRational(ExifTag.MDScalePixel); - case ExifTagValue.CompressedBitsPerPixel: return new ExifRational(ExifTag.CompressedBitsPerPixel); - case ExifTagValue.ApertureValue: return new ExifRational(ExifTag.ApertureValue); - case ExifTagValue.MaxApertureValue: return new ExifRational(ExifTag.MaxApertureValue); - case ExifTagValue.SubjectDistance: return new ExifRational(ExifTag.SubjectDistance); - case ExifTagValue.FocalLength: return new ExifRational(ExifTag.FocalLength); - case ExifTagValue.FlashEnergy2: return new ExifRational(ExifTag.FlashEnergy2); - case ExifTagValue.FocalPlaneXResolution2: return new ExifRational(ExifTag.FocalPlaneXResolution2); - case ExifTagValue.FocalPlaneYResolution2: return new ExifRational(ExifTag.FocalPlaneYResolution2); - case ExifTagValue.ExposureIndex2: return new ExifRational(ExifTag.ExposureIndex2); - case ExifTagValue.Humidity: return new ExifRational(ExifTag.Humidity); - case ExifTagValue.Pressure: return new ExifRational(ExifTag.Pressure); - case ExifTagValue.Acceleration: return new ExifRational(ExifTag.Acceleration); - case ExifTagValue.FlashEnergy: return new ExifRational(ExifTag.FlashEnergy); - case ExifTagValue.FocalPlaneXResolution: return new ExifRational(ExifTag.FocalPlaneXResolution); - case ExifTagValue.FocalPlaneYResolution: return new ExifRational(ExifTag.FocalPlaneYResolution); - case ExifTagValue.ExposureIndex: return new ExifRational(ExifTag.ExposureIndex); - case ExifTagValue.DigitalZoomRatio: return new ExifRational(ExifTag.DigitalZoomRatio); - case ExifTagValue.GPSAltitude: return new ExifRational(ExifTag.GPSAltitude); - case ExifTagValue.GPSDOP: return new ExifRational(ExifTag.GPSDOP); - case ExifTagValue.GPSSpeed: return new ExifRational(ExifTag.GPSSpeed); - case ExifTagValue.GPSTrack: return new ExifRational(ExifTag.GPSTrack); - case ExifTagValue.GPSImgDirection: return new ExifRational(ExifTag.GPSImgDirection); - case ExifTagValue.GPSDestBearing: return new ExifRational(ExifTag.GPSDestBearing); - case ExifTagValue.GPSDestDistance: return new ExifRational(ExifTag.GPSDestDistance); + case ExifTagValue.XPosition: + return new ExifRational(ExifTag.XPosition); + case ExifTagValue.YPosition: + return new ExifRational(ExifTag.YPosition); + case ExifTagValue.XResolution: + return new ExifRational(ExifTag.XResolution); + case ExifTagValue.YResolution: + return new ExifRational(ExifTag.YResolution); + case ExifTagValue.BatteryLevel: + return new ExifRational(ExifTag.BatteryLevel); + case ExifTagValue.ExposureTime: + return new ExifRational(ExifTag.ExposureTime); + case ExifTagValue.FNumber: + return new ExifRational(ExifTag.FNumber); + case ExifTagValue.MDScalePixel: + return new ExifRational(ExifTag.MDScalePixel); + case ExifTagValue.CompressedBitsPerPixel: + return new ExifRational(ExifTag.CompressedBitsPerPixel); + case ExifTagValue.ApertureValue: + return new ExifRational(ExifTag.ApertureValue); + case ExifTagValue.MaxApertureValue: + return new ExifRational(ExifTag.MaxApertureValue); + case ExifTagValue.SubjectDistance: + return new ExifRational(ExifTag.SubjectDistance); + case ExifTagValue.FocalLength: + return new ExifRational(ExifTag.FocalLength); + case ExifTagValue.FlashEnergy2: + return new ExifRational(ExifTag.FlashEnergy2); + case ExifTagValue.FocalPlaneXResolution2: + return new ExifRational(ExifTag.FocalPlaneXResolution2); + case ExifTagValue.FocalPlaneYResolution2: + return new ExifRational(ExifTag.FocalPlaneYResolution2); + case ExifTagValue.ExposureIndex2: + return new ExifRational(ExifTag.ExposureIndex2); + case ExifTagValue.Humidity: + return new ExifRational(ExifTag.Humidity); + case ExifTagValue.Pressure: + return new ExifRational(ExifTag.Pressure); + case ExifTagValue.Acceleration: + return new ExifRational(ExifTag.Acceleration); + case ExifTagValue.FlashEnergy: + return new ExifRational(ExifTag.FlashEnergy); + case ExifTagValue.FocalPlaneXResolution: + return new ExifRational(ExifTag.FocalPlaneXResolution); + case ExifTagValue.FocalPlaneYResolution: + return new ExifRational(ExifTag.FocalPlaneYResolution); + case ExifTagValue.ExposureIndex: + return new ExifRational(ExifTag.ExposureIndex); + case ExifTagValue.DigitalZoomRatio: + return new ExifRational(ExifTag.DigitalZoomRatio); + case ExifTagValue.GPSAltitude: + return new ExifRational(ExifTag.GPSAltitude); + case ExifTagValue.GPSDOP: + return new ExifRational(ExifTag.GPSDOP); + case ExifTagValue.GPSSpeed: + return new ExifRational(ExifTag.GPSSpeed); + case ExifTagValue.GPSTrack: + return new ExifRational(ExifTag.GPSTrack); + case ExifTagValue.GPSImgDirection: + return new ExifRational(ExifTag.GPSImgDirection); + case ExifTagValue.GPSDestBearing: + return new ExifRational(ExifTag.GPSDestBearing); + case ExifTagValue.GPSDestDistance: + return new ExifRational(ExifTag.GPSDestDistance); - case ExifTagValue.WhitePoint: return new ExifRationalArray(ExifTag.WhitePoint); - case ExifTagValue.PrimaryChromaticities: return new ExifRationalArray(ExifTag.PrimaryChromaticities); - case ExifTagValue.YCbCrCoefficients: return new ExifRationalArray(ExifTag.YCbCrCoefficients); - case ExifTagValue.ReferenceBlackWhite: return new ExifRationalArray(ExifTag.ReferenceBlackWhite); - case ExifTagValue.GPSLatitude: return new ExifRationalArray(ExifTag.GPSLatitude); - case ExifTagValue.GPSLongitude: return new ExifRationalArray(ExifTag.GPSLongitude); - case ExifTagValue.GPSTimestamp: return new ExifRationalArray(ExifTag.GPSTimestamp); - case ExifTagValue.GPSDestLatitude: return new ExifRationalArray(ExifTag.GPSDestLatitude); - case ExifTagValue.GPSDestLongitude: return new ExifRationalArray(ExifTag.GPSDestLongitude); - case ExifTagValue.LensSpecification: return new ExifRationalArray(ExifTag.LensSpecification); + case ExifTagValue.WhitePoint: + return new ExifRationalArray(ExifTag.WhitePoint); + case ExifTagValue.PrimaryChromaticities: + return new ExifRationalArray(ExifTag.PrimaryChromaticities); + case ExifTagValue.YCbCrCoefficients: + return new ExifRationalArray(ExifTag.YCbCrCoefficients); + case ExifTagValue.ReferenceBlackWhite: + return new ExifRationalArray(ExifTag.ReferenceBlackWhite); + case ExifTagValue.GPSLatitude: + return new ExifRationalArray(ExifTag.GPSLatitude); + case ExifTagValue.GPSLongitude: + return new ExifRationalArray(ExifTag.GPSLongitude); + case ExifTagValue.GPSTimestamp: + return new ExifRationalArray(ExifTag.GPSTimestamp); + case ExifTagValue.GPSDestLatitude: + return new ExifRationalArray(ExifTag.GPSDestLatitude); + case ExifTagValue.GPSDestLongitude: + return new ExifRationalArray(ExifTag.GPSDestLongitude); + case ExifTagValue.LensSpecification: + return new ExifRationalArray(ExifTag.LensSpecification); - case ExifTagValue.OldSubfileType: return new ExifShort(ExifTag.OldSubfileType); - case ExifTagValue.Compression: return new ExifShort(ExifTag.Compression); - case ExifTagValue.PhotometricInterpretation: return new ExifShort(ExifTag.PhotometricInterpretation); - case ExifTagValue.Thresholding: return new ExifShort(ExifTag.Thresholding); - case ExifTagValue.CellWidth: return new ExifShort(ExifTag.CellWidth); - case ExifTagValue.CellLength: return new ExifShort(ExifTag.CellLength); - case ExifTagValue.FillOrder: return new ExifShort(ExifTag.FillOrder); - case ExifTagValue.Orientation: return new ExifShort(ExifTag.Orientation); - case ExifTagValue.SamplesPerPixel: return new ExifShort(ExifTag.SamplesPerPixel); - case ExifTagValue.PlanarConfiguration: return new ExifShort(ExifTag.PlanarConfiguration); - case ExifTagValue.Predictor: return new ExifShort(ExifTag.Predictor); - case ExifTagValue.GrayResponseUnit: return new ExifShort(ExifTag.GrayResponseUnit); - case ExifTagValue.ResolutionUnit: return new ExifShort(ExifTag.ResolutionUnit); - case ExifTagValue.CleanFaxData: return new ExifShort(ExifTag.CleanFaxData); - case ExifTagValue.InkSet: return new ExifShort(ExifTag.InkSet); - case ExifTagValue.NumberOfInks: return new ExifShort(ExifTag.NumberOfInks); - case ExifTagValue.DotRange: return new ExifShort(ExifTag.DotRange); - case ExifTagValue.Indexed: return new ExifShort(ExifTag.Indexed); - case ExifTagValue.OPIProxy: return new ExifShort(ExifTag.OPIProxy); - case ExifTagValue.JPEGProc: return new ExifShort(ExifTag.JPEGProc); - case ExifTagValue.JPEGRestartInterval: return new ExifShort(ExifTag.JPEGRestartInterval); - case ExifTagValue.YCbCrPositioning: return new ExifShort(ExifTag.YCbCrPositioning); - case ExifTagValue.Rating: return new ExifShort(ExifTag.Rating); - case ExifTagValue.RatingPercent: return new ExifShort(ExifTag.RatingPercent); - case ExifTagValue.ExposureProgram: return new ExifShort(ExifTag.ExposureProgram); - case ExifTagValue.Interlace: return new ExifShort(ExifTag.Interlace); - case ExifTagValue.SelfTimerMode: return new ExifShort(ExifTag.SelfTimerMode); - case ExifTagValue.SensitivityType: return new ExifShort(ExifTag.SensitivityType); - case ExifTagValue.MeteringMode: return new ExifShort(ExifTag.MeteringMode); - case ExifTagValue.LightSource: return new ExifShort(ExifTag.LightSource); - case ExifTagValue.FocalPlaneResolutionUnit2: return new ExifShort(ExifTag.FocalPlaneResolutionUnit2); - case ExifTagValue.SensingMethod2: return new ExifShort(ExifTag.SensingMethod2); - case ExifTagValue.Flash: return new ExifShort(ExifTag.Flash); - case ExifTagValue.ColorSpace: return new ExifShort(ExifTag.ColorSpace); - case ExifTagValue.FocalPlaneResolutionUnit: return new ExifShort(ExifTag.FocalPlaneResolutionUnit); - case ExifTagValue.SensingMethod: return new ExifShort(ExifTag.SensingMethod); - case ExifTagValue.CustomRendered: return new ExifShort(ExifTag.CustomRendered); - case ExifTagValue.ExposureMode: return new ExifShort(ExifTag.ExposureMode); - case ExifTagValue.WhiteBalance: return new ExifShort(ExifTag.WhiteBalance); - case ExifTagValue.FocalLengthIn35mmFilm: return new ExifShort(ExifTag.FocalLengthIn35mmFilm); - case ExifTagValue.SceneCaptureType: return new ExifShort(ExifTag.SceneCaptureType); - case ExifTagValue.GainControl: return new ExifShort(ExifTag.GainControl); - case ExifTagValue.Contrast: return new ExifShort(ExifTag.Contrast); - case ExifTagValue.Saturation: return new ExifShort(ExifTag.Saturation); - case ExifTagValue.Sharpness: return new ExifShort(ExifTag.Sharpness); - case ExifTagValue.SubjectDistanceRange: return new ExifShort(ExifTag.SubjectDistanceRange); - case ExifTagValue.GPSDifferential: return new ExifShort(ExifTag.GPSDifferential); + case ExifTagValue.OldSubfileType: + return new ExifShort(ExifTag.OldSubfileType); + case ExifTagValue.Compression: + return new ExifShort(ExifTag.Compression); + case ExifTagValue.PhotometricInterpretation: + return new ExifShort(ExifTag.PhotometricInterpretation); + case ExifTagValue.Thresholding: + return new ExifShort(ExifTag.Thresholding); + case ExifTagValue.CellWidth: + return new ExifShort(ExifTag.CellWidth); + case ExifTagValue.CellLength: + return new ExifShort(ExifTag.CellLength); + case ExifTagValue.FillOrder: + return new ExifShort(ExifTag.FillOrder); + case ExifTagValue.Orientation: + return new ExifShort(ExifTag.Orientation); + case ExifTagValue.SamplesPerPixel: + return new ExifShort(ExifTag.SamplesPerPixel); + case ExifTagValue.PlanarConfiguration: + return new ExifShort(ExifTag.PlanarConfiguration); + case ExifTagValue.Predictor: + return new ExifShort(ExifTag.Predictor); + case ExifTagValue.GrayResponseUnit: + return new ExifShort(ExifTag.GrayResponseUnit); + case ExifTagValue.ResolutionUnit: + return new ExifShort(ExifTag.ResolutionUnit); + case ExifTagValue.CleanFaxData: + return new ExifShort(ExifTag.CleanFaxData); + case ExifTagValue.InkSet: + return new ExifShort(ExifTag.InkSet); + case ExifTagValue.NumberOfInks: + return new ExifShort(ExifTag.NumberOfInks); + case ExifTagValue.DotRange: + return new ExifShort(ExifTag.DotRange); + case ExifTagValue.Indexed: + return new ExifShort(ExifTag.Indexed); + case ExifTagValue.OPIProxy: + return new ExifShort(ExifTag.OPIProxy); + case ExifTagValue.JPEGProc: + return new ExifShort(ExifTag.JPEGProc); + case ExifTagValue.JPEGRestartInterval: + return new ExifShort(ExifTag.JPEGRestartInterval); + case ExifTagValue.YCbCrPositioning: + return new ExifShort(ExifTag.YCbCrPositioning); + case ExifTagValue.Rating: + return new ExifShort(ExifTag.Rating); + case ExifTagValue.RatingPercent: + return new ExifShort(ExifTag.RatingPercent); + case ExifTagValue.ExposureProgram: + return new ExifShort(ExifTag.ExposureProgram); + case ExifTagValue.Interlace: + return new ExifShort(ExifTag.Interlace); + case ExifTagValue.SelfTimerMode: + return new ExifShort(ExifTag.SelfTimerMode); + case ExifTagValue.SensitivityType: + return new ExifShort(ExifTag.SensitivityType); + case ExifTagValue.MeteringMode: + return new ExifShort(ExifTag.MeteringMode); + case ExifTagValue.LightSource: + return new ExifShort(ExifTag.LightSource); + case ExifTagValue.FocalPlaneResolutionUnit2: + return new ExifShort(ExifTag.FocalPlaneResolutionUnit2); + case ExifTagValue.SensingMethod2: + return new ExifShort(ExifTag.SensingMethod2); + case ExifTagValue.Flash: + return new ExifShort(ExifTag.Flash); + case ExifTagValue.ColorSpace: + return new ExifShort(ExifTag.ColorSpace); + case ExifTagValue.FocalPlaneResolutionUnit: + return new ExifShort(ExifTag.FocalPlaneResolutionUnit); + case ExifTagValue.SensingMethod: + return new ExifShort(ExifTag.SensingMethod); + case ExifTagValue.CustomRendered: + return new ExifShort(ExifTag.CustomRendered); + case ExifTagValue.ExposureMode: + return new ExifShort(ExifTag.ExposureMode); + case ExifTagValue.WhiteBalance: + return new ExifShort(ExifTag.WhiteBalance); + case ExifTagValue.FocalLengthIn35mmFilm: + return new ExifShort(ExifTag.FocalLengthIn35mmFilm); + case ExifTagValue.SceneCaptureType: + return new ExifShort(ExifTag.SceneCaptureType); + case ExifTagValue.GainControl: + return new ExifShort(ExifTag.GainControl); + case ExifTagValue.Contrast: + return new ExifShort(ExifTag.Contrast); + case ExifTagValue.Saturation: + return new ExifShort(ExifTag.Saturation); + case ExifTagValue.Sharpness: + return new ExifShort(ExifTag.Sharpness); + case ExifTagValue.SubjectDistanceRange: + return new ExifShort(ExifTag.SubjectDistanceRange); + case ExifTagValue.GPSDifferential: + return new ExifShort(ExifTag.GPSDifferential); - case ExifTagValue.BitsPerSample: return new ExifShortArray(ExifTag.BitsPerSample); - case ExifTagValue.MinSampleValue: return new ExifShortArray(ExifTag.MinSampleValue); - case ExifTagValue.MaxSampleValue: return new ExifShortArray(ExifTag.MaxSampleValue); - case ExifTagValue.GrayResponseCurve: return new ExifShortArray(ExifTag.GrayResponseCurve); - case ExifTagValue.ColorMap: return new ExifShortArray(ExifTag.ColorMap); - case ExifTagValue.ExtraSamples: return new ExifShortArray(ExifTag.ExtraSamples); - case ExifTagValue.PageNumber: return new ExifShortArray(ExifTag.PageNumber); - case ExifTagValue.TransferFunction: return new ExifShortArray(ExifTag.TransferFunction); - case ExifTagValue.HalftoneHints: return new ExifShortArray(ExifTag.HalftoneHints); - case ExifTagValue.SampleFormat: return new ExifShortArray(ExifTag.SampleFormat); - case ExifTagValue.TransferRange: return new ExifShortArray(ExifTag.TransferRange); - case ExifTagValue.DefaultImageColor: return new ExifShortArray(ExifTag.DefaultImageColor); - case ExifTagValue.JPEGLosslessPredictors: return new ExifShortArray(ExifTag.JPEGLosslessPredictors); - case ExifTagValue.JPEGPointTransforms: return new ExifShortArray(ExifTag.JPEGPointTransforms); - case ExifTagValue.YCbCrSubsampling: return new ExifShortArray(ExifTag.YCbCrSubsampling); - case ExifTagValue.CFARepeatPatternDim: return new ExifShortArray(ExifTag.CFARepeatPatternDim); - case ExifTagValue.IntergraphPacketData: return new ExifShortArray(ExifTag.IntergraphPacketData); - case ExifTagValue.ISOSpeedRatings: return new ExifShortArray(ExifTag.ISOSpeedRatings); - case ExifTagValue.SubjectArea: return new ExifShortArray(ExifTag.SubjectArea); - case ExifTagValue.SubjectLocation: return new ExifShortArray(ExifTag.SubjectLocation); + case ExifTagValue.BitsPerSample: + return new ExifShortArray(ExifTag.BitsPerSample); + case ExifTagValue.MinSampleValue: + return new ExifShortArray(ExifTag.MinSampleValue); + case ExifTagValue.MaxSampleValue: + return new ExifShortArray(ExifTag.MaxSampleValue); + case ExifTagValue.GrayResponseCurve: + return new ExifShortArray(ExifTag.GrayResponseCurve); + case ExifTagValue.ColorMap: + return new ExifShortArray(ExifTag.ColorMap); + case ExifTagValue.ExtraSamples: + return new ExifShortArray(ExifTag.ExtraSamples); + case ExifTagValue.PageNumber: + return new ExifShortArray(ExifTag.PageNumber); + case ExifTagValue.TransferFunction: + return new ExifShortArray(ExifTag.TransferFunction); + case ExifTagValue.HalftoneHints: + return new ExifShortArray(ExifTag.HalftoneHints); + case ExifTagValue.SampleFormat: + return new ExifShortArray(ExifTag.SampleFormat); + case ExifTagValue.TransferRange: + return new ExifShortArray(ExifTag.TransferRange); + case ExifTagValue.DefaultImageColor: + return new ExifShortArray(ExifTag.DefaultImageColor); + case ExifTagValue.JPEGLosslessPredictors: + return new ExifShortArray(ExifTag.JPEGLosslessPredictors); + case ExifTagValue.JPEGPointTransforms: + return new ExifShortArray(ExifTag.JPEGPointTransforms); + case ExifTagValue.YCbCrSubsampling: + return new ExifShortArray(ExifTag.YCbCrSubsampling); + case ExifTagValue.CFARepeatPatternDim: + return new ExifShortArray(ExifTag.CFARepeatPatternDim); + case ExifTagValue.IntergraphPacketData: + return new ExifShortArray(ExifTag.IntergraphPacketData); + case ExifTagValue.ISOSpeedRatings: + return new ExifShortArray(ExifTag.ISOSpeedRatings); + case ExifTagValue.SubjectArea: + return new ExifShortArray(ExifTag.SubjectArea); + case ExifTagValue.SubjectLocation: + return new ExifShortArray(ExifTag.SubjectLocation); - case ExifTagValue.ShutterSpeedValue: return new ExifSignedRational(ExifTag.ShutterSpeedValue); - case ExifTagValue.BrightnessValue: return new ExifSignedRational(ExifTag.BrightnessValue); - case ExifTagValue.ExposureBiasValue: return new ExifSignedRational(ExifTag.ExposureBiasValue); - case ExifTagValue.AmbientTemperature: return new ExifSignedRational(ExifTag.AmbientTemperature); - case ExifTagValue.WaterDepth: return new ExifSignedRational(ExifTag.WaterDepth); - case ExifTagValue.CameraElevationAngle: return new ExifSignedRational(ExifTag.CameraElevationAngle); + case ExifTagValue.ShutterSpeedValue: + return new ExifSignedRational(ExifTag.ShutterSpeedValue); + case ExifTagValue.BrightnessValue: + return new ExifSignedRational(ExifTag.BrightnessValue); + case ExifTagValue.ExposureBiasValue: + return new ExifSignedRational(ExifTag.ExposureBiasValue); + case ExifTagValue.AmbientTemperature: + return new ExifSignedRational(ExifTag.AmbientTemperature); + case ExifTagValue.WaterDepth: + return new ExifSignedRational(ExifTag.WaterDepth); + case ExifTagValue.CameraElevationAngle: + return new ExifSignedRational(ExifTag.CameraElevationAngle); - case ExifTagValue.Decode: return new ExifSignedRationalArray(ExifTag.Decode); + case ExifTagValue.Decode: + return new ExifSignedRationalArray(ExifTag.Decode); - case ExifTagValue.ImageDescription: return new ExifString(ExifTag.ImageDescription); - case ExifTagValue.Make: return new ExifString(ExifTag.Make); - case ExifTagValue.Model: return new ExifString(ExifTag.Model); - case ExifTagValue.Software: return new ExifString(ExifTag.Software); - case ExifTagValue.DateTime: return new ExifString(ExifTag.DateTime); - case ExifTagValue.Artist: return new ExifString(ExifTag.Artist); - case ExifTagValue.HostComputer: return new ExifString(ExifTag.HostComputer); - case ExifTagValue.Copyright: return new ExifString(ExifTag.Copyright); - case ExifTagValue.DocumentName: return new ExifString(ExifTag.DocumentName); - case ExifTagValue.PageName: return new ExifString(ExifTag.PageName); - case ExifTagValue.InkNames: return new ExifString(ExifTag.InkNames); - case ExifTagValue.TargetPrinter: return new ExifString(ExifTag.TargetPrinter); - case ExifTagValue.ImageID: return new ExifString(ExifTag.ImageID); - case ExifTagValue.MDLabName: return new ExifString(ExifTag.MDLabName); - case ExifTagValue.MDSampleInfo: return new ExifString(ExifTag.MDSampleInfo); - case ExifTagValue.MDPrepDate: return new ExifString(ExifTag.MDPrepDate); - case ExifTagValue.MDPrepTime: return new ExifString(ExifTag.MDPrepTime); - case ExifTagValue.MDFileUnits: return new ExifString(ExifTag.MDFileUnits); - case ExifTagValue.SEMInfo: return new ExifString(ExifTag.SEMInfo); - case ExifTagValue.SpectralSensitivity: return new ExifString(ExifTag.SpectralSensitivity); - case ExifTagValue.DateTimeOriginal: return new ExifString(ExifTag.DateTimeOriginal); - case ExifTagValue.DateTimeDigitized: return new ExifString(ExifTag.DateTimeDigitized); - case ExifTagValue.SubsecTime: return new ExifString(ExifTag.SubsecTime); - case ExifTagValue.SubsecTimeOriginal: return new ExifString(ExifTag.SubsecTimeOriginal); - case ExifTagValue.SubsecTimeDigitized: return new ExifString(ExifTag.SubsecTimeDigitized); - case ExifTagValue.RelatedSoundFile: return new ExifString(ExifTag.RelatedSoundFile); - case ExifTagValue.FaxSubaddress: return new ExifString(ExifTag.FaxSubaddress); - case ExifTagValue.OffsetTime: return new ExifString(ExifTag.OffsetTime); - case ExifTagValue.OffsetTimeOriginal: return new ExifString(ExifTag.OffsetTimeOriginal); - case ExifTagValue.OffsetTimeDigitized: return new ExifString(ExifTag.OffsetTimeDigitized); - case ExifTagValue.SecurityClassification: return new ExifString(ExifTag.SecurityClassification); - case ExifTagValue.ImageHistory: return new ExifString(ExifTag.ImageHistory); - case ExifTagValue.ImageUniqueID: return new ExifString(ExifTag.ImageUniqueID); - case ExifTagValue.OwnerName: return new ExifString(ExifTag.OwnerName); - case ExifTagValue.SerialNumber: return new ExifString(ExifTag.SerialNumber); - case ExifTagValue.LensMake: return new ExifString(ExifTag.LensMake); - case ExifTagValue.LensModel: return new ExifString(ExifTag.LensModel); - case ExifTagValue.LensSerialNumber: return new ExifString(ExifTag.LensSerialNumber); - case ExifTagValue.GDALMetadata: return new ExifString(ExifTag.GDALMetadata); - case ExifTagValue.GDALNoData: return new ExifString(ExifTag.GDALNoData); - case ExifTagValue.GPSLatitudeRef: return new ExifString(ExifTag.GPSLatitudeRef); - case ExifTagValue.GPSLongitudeRef: return new ExifString(ExifTag.GPSLongitudeRef); - case ExifTagValue.GPSSatellites: return new ExifString(ExifTag.GPSSatellites); - case ExifTagValue.GPSStatus: return new ExifString(ExifTag.GPSStatus); - case ExifTagValue.GPSMeasureMode: return new ExifString(ExifTag.GPSMeasureMode); - case ExifTagValue.GPSSpeedRef: return new ExifString(ExifTag.GPSSpeedRef); - case ExifTagValue.GPSTrackRef: return new ExifString(ExifTag.GPSTrackRef); - case ExifTagValue.GPSImgDirectionRef: return new ExifString(ExifTag.GPSImgDirectionRef); - case ExifTagValue.GPSMapDatum: return new ExifString(ExifTag.GPSMapDatum); - case ExifTagValue.GPSDestLatitudeRef: return new ExifString(ExifTag.GPSDestLatitudeRef); - case ExifTagValue.GPSDestLongitudeRef: return new ExifString(ExifTag.GPSDestLongitudeRef); - case ExifTagValue.GPSDestBearingRef: return new ExifString(ExifTag.GPSDestBearingRef); - case ExifTagValue.GPSDestDistanceRef: return new ExifString(ExifTag.GPSDestDistanceRef); - case ExifTagValue.GPSDateStamp: return new ExifString(ExifTag.GPSDateStamp); + case ExifTagValue.ImageDescription: + return new ExifString(ExifTag.ImageDescription); + case ExifTagValue.Make: + return new ExifString(ExifTag.Make); + case ExifTagValue.Model: + return new ExifString(ExifTag.Model); + case ExifTagValue.Software: + return new ExifString(ExifTag.Software); + case ExifTagValue.DateTime: + return new ExifString(ExifTag.DateTime); + case ExifTagValue.Artist: + return new ExifString(ExifTag.Artist); + case ExifTagValue.HostComputer: + return new ExifString(ExifTag.HostComputer); + case ExifTagValue.Copyright: + return new ExifString(ExifTag.Copyright); + case ExifTagValue.DocumentName: + return new ExifString(ExifTag.DocumentName); + case ExifTagValue.PageName: + return new ExifString(ExifTag.PageName); + case ExifTagValue.InkNames: + return new ExifString(ExifTag.InkNames); + case ExifTagValue.TargetPrinter: + return new ExifString(ExifTag.TargetPrinter); + case ExifTagValue.ImageID: + return new ExifString(ExifTag.ImageID); + case ExifTagValue.MDLabName: + return new ExifString(ExifTag.MDLabName); + case ExifTagValue.MDSampleInfo: + return new ExifString(ExifTag.MDSampleInfo); + case ExifTagValue.MDPrepDate: + return new ExifString(ExifTag.MDPrepDate); + case ExifTagValue.MDPrepTime: + return new ExifString(ExifTag.MDPrepTime); + case ExifTagValue.MDFileUnits: + return new ExifString(ExifTag.MDFileUnits); + case ExifTagValue.SEMInfo: + return new ExifString(ExifTag.SEMInfo); + case ExifTagValue.SpectralSensitivity: + return new ExifString(ExifTag.SpectralSensitivity); + case ExifTagValue.DateTimeOriginal: + return new ExifString(ExifTag.DateTimeOriginal); + case ExifTagValue.DateTimeDigitized: + return new ExifString(ExifTag.DateTimeDigitized); + case ExifTagValue.SubsecTime: + return new ExifString(ExifTag.SubsecTime); + case ExifTagValue.SubsecTimeOriginal: + return new ExifString(ExifTag.SubsecTimeOriginal); + case ExifTagValue.SubsecTimeDigitized: + return new ExifString(ExifTag.SubsecTimeDigitized); + case ExifTagValue.RelatedSoundFile: + return new ExifString(ExifTag.RelatedSoundFile); + case ExifTagValue.FaxSubaddress: + return new ExifString(ExifTag.FaxSubaddress); + case ExifTagValue.OffsetTime: + return new ExifString(ExifTag.OffsetTime); + case ExifTagValue.OffsetTimeOriginal: + return new ExifString(ExifTag.OffsetTimeOriginal); + case ExifTagValue.OffsetTimeDigitized: + return new ExifString(ExifTag.OffsetTimeDigitized); + case ExifTagValue.SecurityClassification: + return new ExifString(ExifTag.SecurityClassification); + case ExifTagValue.ImageHistory: + return new ExifString(ExifTag.ImageHistory); + case ExifTagValue.ImageUniqueID: + return new ExifString(ExifTag.ImageUniqueID); + case ExifTagValue.OwnerName: + return new ExifString(ExifTag.OwnerName); + case ExifTagValue.SerialNumber: + return new ExifString(ExifTag.SerialNumber); + case ExifTagValue.LensMake: + return new ExifString(ExifTag.LensMake); + case ExifTagValue.LensModel: + return new ExifString(ExifTag.LensModel); + case ExifTagValue.LensSerialNumber: + return new ExifString(ExifTag.LensSerialNumber); + case ExifTagValue.GDALMetadata: + return new ExifString(ExifTag.GDALMetadata); + case ExifTagValue.GDALNoData: + return new ExifString(ExifTag.GDALNoData); + case ExifTagValue.GPSLatitudeRef: + return new ExifString(ExifTag.GPSLatitudeRef); + case ExifTagValue.GPSLongitudeRef: + return new ExifString(ExifTag.GPSLongitudeRef); + case ExifTagValue.GPSSatellites: + return new ExifString(ExifTag.GPSSatellites); + case ExifTagValue.GPSStatus: + return new ExifString(ExifTag.GPSStatus); + case ExifTagValue.GPSMeasureMode: + return new ExifString(ExifTag.GPSMeasureMode); + case ExifTagValue.GPSSpeedRef: + return new ExifString(ExifTag.GPSSpeedRef); + case ExifTagValue.GPSTrackRef: + return new ExifString(ExifTag.GPSTrackRef); + case ExifTagValue.GPSImgDirectionRef: + return new ExifString(ExifTag.GPSImgDirectionRef); + case ExifTagValue.GPSMapDatum: + return new ExifString(ExifTag.GPSMapDatum); + case ExifTagValue.GPSDestLatitudeRef: + return new ExifString(ExifTag.GPSDestLatitudeRef); + case ExifTagValue.GPSDestLongitudeRef: + return new ExifString(ExifTag.GPSDestLongitudeRef); + case ExifTagValue.GPSDestBearingRef: + return new ExifString(ExifTag.GPSDestBearingRef); + case ExifTagValue.GPSDestDistanceRef: + return new ExifString(ExifTag.GPSDestDistanceRef); + case ExifTagValue.GPSDateStamp: + return new ExifString(ExifTag.GPSDateStamp); - case ExifTagValue.FileSource: return new ExifByte(ExifTag.FileSource, ExifDataType.Undefined); - case ExifTagValue.SceneType: return new ExifByte(ExifTag.SceneType, ExifDataType.Undefined); + case ExifTagValue.FileSource: + return new ExifByte(ExifTag.FileSource, ExifDataType.Undefined); + case ExifTagValue.SceneType: + return new ExifByte(ExifTag.SceneType, ExifDataType.Undefined); - case ExifTagValue.JPEGTables: return new ExifByteArray(ExifTag.JPEGTables, ExifDataType.Undefined); - case ExifTagValue.OECF: return new ExifByteArray(ExifTag.OECF, ExifDataType.Undefined); - case ExifTagValue.ExifVersion: return new ExifByteArray(ExifTag.ExifVersion, ExifDataType.Undefined); - case ExifTagValue.ComponentsConfiguration: return new ExifByteArray(ExifTag.ComponentsConfiguration, ExifDataType.Undefined); - case ExifTagValue.MakerNote: return new ExifByteArray(ExifTag.MakerNote, ExifDataType.Undefined); - case ExifTagValue.UserComment: return new ExifByteArray(ExifTag.UserComment, ExifDataType.Undefined); - case ExifTagValue.FlashpixVersion: return new ExifByteArray(ExifTag.FlashpixVersion, ExifDataType.Undefined); - case ExifTagValue.SpatialFrequencyResponse: return new ExifByteArray(ExifTag.SpatialFrequencyResponse, ExifDataType.Undefined); - case ExifTagValue.SpatialFrequencyResponse2: return new ExifByteArray(ExifTag.SpatialFrequencyResponse2, ExifDataType.Undefined); - case ExifTagValue.Noise: return new ExifByteArray(ExifTag.Noise, ExifDataType.Undefined); - case ExifTagValue.CFAPattern: return new ExifByteArray(ExifTag.CFAPattern, ExifDataType.Undefined); - case ExifTagValue.DeviceSettingDescription: return new ExifByteArray(ExifTag.DeviceSettingDescription, ExifDataType.Undefined); - case ExifTagValue.ImageSourceData: return new ExifByteArray(ExifTag.ImageSourceData, ExifDataType.Undefined); - case ExifTagValue.GPSProcessingMethod: return new ExifByteArray(ExifTag.GPSProcessingMethod, ExifDataType.Undefined); - case ExifTagValue.GPSAreaInformation: return new ExifByteArray(ExifTag.GPSAreaInformation, ExifDataType.Undefined); + case ExifTagValue.JPEGTables: + return new ExifByteArray(ExifTag.JPEGTables, ExifDataType.Undefined); + case ExifTagValue.OECF: + return new ExifByteArray(ExifTag.OECF, ExifDataType.Undefined); + case ExifTagValue.ExifVersion: + return new ExifByteArray(ExifTag.ExifVersion, ExifDataType.Undefined); + case ExifTagValue.ComponentsConfiguration: + return new ExifByteArray(ExifTag.ComponentsConfiguration, ExifDataType.Undefined); + case ExifTagValue.MakerNote: + return new ExifByteArray(ExifTag.MakerNote, ExifDataType.Undefined); + case ExifTagValue.FlashpixVersion: + return new ExifByteArray(ExifTag.FlashpixVersion, ExifDataType.Undefined); + case ExifTagValue.SpatialFrequencyResponse: + return new ExifByteArray(ExifTag.SpatialFrequencyResponse, ExifDataType.Undefined); + case ExifTagValue.SpatialFrequencyResponse2: + return new ExifByteArray(ExifTag.SpatialFrequencyResponse2, ExifDataType.Undefined); + case ExifTagValue.Noise: + return new ExifByteArray(ExifTag.Noise, ExifDataType.Undefined); + case ExifTagValue.CFAPattern: + return new ExifByteArray(ExifTag.CFAPattern, ExifDataType.Undefined); + case ExifTagValue.DeviceSettingDescription: + return new ExifByteArray(ExifTag.DeviceSettingDescription, ExifDataType.Undefined); + case ExifTagValue.ImageSourceData: + return new ExifByteArray(ExifTag.ImageSourceData, ExifDataType.Undefined); - default: return null; + case ExifTagValue.XPTitle: + return new ExifUcs2String(ExifTag.XPTitle); + case ExifTagValue.XPComment: + return new ExifUcs2String(ExifTag.XPComment); + case ExifTagValue.XPAuthor: + return new ExifUcs2String(ExifTag.XPAuthor); + case ExifTagValue.XPKeywords: + return new ExifUcs2String(ExifTag.XPKeywords); + case ExifTagValue.XPSubject: + return new ExifUcs2String(ExifTag.XPSubject); + + case ExifTagValue.UserComment: + return new ExifEncodedString(ExifTag.UserComment); + case ExifTagValue.GPSProcessingMethod: + return new ExifEncodedString(ExifTag.GPSProcessingMethod); + case ExifTagValue.GPSAreaInformation: + return new ExifEncodedString(ExifTag.GPSAreaInformation); + + default: + return null; } } } diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaVector.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaVector.PixelOperations.cs index c66526bbe..e78a644af 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaVector.PixelOperations.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/RgbaVector.PixelOperations.cs @@ -21,7 +21,7 @@ namespace SixLabors.ImageSharp.PixelFormats internal class PixelOperations : PixelOperations { private static readonly Lazy LazyInfo = - new Lazy(() => PixelTypeInfo.Create(PixelAlphaRepresentation.Unassociated), true); + new(() => PixelTypeInfo.Create(PixelAlphaRepresentation.Unassociated), true); /// public override PixelTypeInfo GetPixelTypeInfo() => LazyInfo.Value; @@ -34,7 +34,7 @@ namespace SixLabors.ImageSharp.PixelFormats { Span destinationVectors = MemoryMarshal.Cast(destinationPixels); - PixelOperations.Instance.ToVector4(configuration, sourcePixels, destinationVectors); + PixelOperations.Instance.ToVector4(configuration, sourcePixels, destinationVectors, PixelConversionModifiers.Scale); } /// diff --git a/src/ImageSharp/PixelFormats/PixelOperations{TPixel}.cs b/src/ImageSharp/PixelFormats/PixelOperations{TPixel}.cs index f748a4b57..710eb9c08 100644 --- a/src/ImageSharp/PixelFormats/PixelOperations{TPixel}.cs +++ b/src/ImageSharp/PixelFormats/PixelOperations{TPixel}.cs @@ -19,7 +19,7 @@ namespace SixLabors.ImageSharp.PixelFormats public partial class PixelOperations where TPixel : unmanaged, IPixel { - private static readonly Lazy LazyInfo = new Lazy(() => PixelTypeInfo.Create(), true); + private static readonly Lazy LazyInfo = new(() => PixelTypeInfo.Create(), true); /// /// Gets the global instance for the pixel type @@ -116,29 +116,29 @@ namespace SixLabors.ImageSharp.PixelFormats Span destinationPixels) where TSourcePixel : unmanaged, IPixel { - const int SliceLength = 1024; - int numberOfSlices = sourcePixels.Length / SliceLength; + const int sliceLength = 1024; + int numberOfSlices = sourcePixels.Length / sliceLength; - using IMemoryOwner tempVectors = configuration.MemoryAllocator.Allocate(SliceLength); + using IMemoryOwner tempVectors = configuration.MemoryAllocator.Allocate(sliceLength); Span vectorSpan = tempVectors.GetSpan(); for (int i = 0; i < numberOfSlices; i++) { - int start = i * SliceLength; - ReadOnlySpan s = sourcePixels.Slice(start, SliceLength); - Span d = destinationPixels.Slice(start, SliceLength); - PixelOperations.Instance.ToVector4(configuration, s, vectorSpan); - this.FromVector4Destructive(configuration, vectorSpan, d); + int start = i * sliceLength; + ReadOnlySpan s = sourcePixels.Slice(start, sliceLength); + Span d = destinationPixels.Slice(start, sliceLength); + PixelOperations.Instance.ToVector4(configuration, s, vectorSpan, PixelConversionModifiers.Scale); + this.FromVector4Destructive(configuration, vectorSpan, d, PixelConversionModifiers.Scale); } - int endOfCompleteSlices = numberOfSlices * SliceLength; + int endOfCompleteSlices = numberOfSlices * sliceLength; int remainder = sourcePixels.Length - endOfCompleteSlices; if (remainder > 0) { ReadOnlySpan s = sourcePixels.Slice(endOfCompleteSlices); Span d = destinationPixels.Slice(endOfCompleteSlices); vectorSpan = vectorSpan.Slice(0, remainder); - PixelOperations.Instance.ToVector4(configuration, s, vectorSpan); - this.FromVector4Destructive(configuration, vectorSpan, d); + PixelOperations.Instance.ToVector4(configuration, s, vectorSpan, PixelConversionModifiers.Scale); + this.FromVector4Destructive(configuration, vectorSpan, d, PixelConversionModifiers.Scale); } } diff --git a/src/ImageSharp/Processing/Extensions/Convolution/BoxBlurExtensions.cs b/src/ImageSharp/Processing/Extensions/Convolution/BoxBlurExtensions.cs index f891ffa97..a7f93e23e 100644 --- a/src/ImageSharp/Processing/Extensions/Convolution/BoxBlurExtensions.cs +++ b/src/ImageSharp/Processing/Extensions/Convolution/BoxBlurExtensions.cs @@ -39,5 +39,26 @@ namespace SixLabors.ImageSharp.Processing /// The to allow chaining of operations. public static IImageProcessingContext BoxBlur(this IImageProcessingContext source, int radius, Rectangle rectangle) => source.ApplyProcessor(new BoxBlurProcessor(radius), rectangle); + + /// + /// Applies a box blur to the image. + /// + /// The image this method extends. + /// The 'radius' value representing the size of the area to sample. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// + /// The to use when mapping the pixels outside of the border, in X direction. + /// + /// + /// The to use when mapping the pixels outside of the border, in Y direction. + /// + /// The to allow chaining of operations. + public static IImageProcessingContext BoxBlur(this IImageProcessingContext source, int radius, Rectangle rectangle, BorderWrappingMode borderWrapModeX, BorderWrappingMode borderWrapModeY) + { + var processor = new BoxBlurProcessor(radius, borderWrapModeX, borderWrapModeY); + return source.ApplyProcessor(processor, rectangle); + } } } diff --git a/src/ImageSharp/Processing/Extensions/Convolution/GaussianBlurExtensions.cs b/src/ImageSharp/Processing/Extensions/Convolution/GaussianBlurExtensions.cs index bd4fb716d..49af591e9 100644 --- a/src/ImageSharp/Processing/Extensions/Convolution/GaussianBlurExtensions.cs +++ b/src/ImageSharp/Processing/Extensions/Convolution/GaussianBlurExtensions.cs @@ -39,5 +39,26 @@ namespace SixLabors.ImageSharp.Processing /// The to allow chaining of operations. public static IImageProcessingContext GaussianBlur(this IImageProcessingContext source, float sigma, Rectangle rectangle) => source.ApplyProcessor(new GaussianBlurProcessor(sigma), rectangle); + + /// + /// Applies a Gaussian blur to the image. + /// + /// The image this method extends. + /// The 'sigma' value representing the weight of the blur. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// + /// The to use when mapping the pixels outside of the border, in X direction. + /// + /// + /// The to use when mapping the pixels outside of the border, in Y direction. + /// + /// The to allow chaining of operations. + public static IImageProcessingContext GaussianBlur(this IImageProcessingContext source, float sigma, Rectangle rectangle, BorderWrappingMode borderWrapModeX, BorderWrappingMode borderWrapModeY) + { + var processor = new GaussianBlurProcessor(sigma, borderWrapModeX, borderWrapModeY); + return source.ApplyProcessor(processor, rectangle); + } } } diff --git a/src/ImageSharp/Processing/Extensions/Convolution/GaussianSharpenExtensions.cs b/src/ImageSharp/Processing/Extensions/Convolution/GaussianSharpenExtensions.cs index f5b8798f4..5ac9d6909 100644 --- a/src/ImageSharp/Processing/Extensions/Convolution/GaussianSharpenExtensions.cs +++ b/src/ImageSharp/Processing/Extensions/Convolution/GaussianSharpenExtensions.cs @@ -42,5 +42,26 @@ namespace SixLabors.ImageSharp.Processing float sigma, Rectangle rectangle) => source.ApplyProcessor(new GaussianSharpenProcessor(sigma), rectangle); + + /// + /// Applies a Gaussian sharpening filter to the image. + /// + /// The image this method extends. + /// The 'sigma' value representing the weight of the blur. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// + /// The to use when mapping the pixels outside of the border, in X direction. + /// + /// + /// The to use when mapping the pixels outside of the border, in Y direction. + /// + /// The to allow chaining of operations. + public static IImageProcessingContext GaussianSharpen(this IImageProcessingContext source, float sigma, Rectangle rectangle, BorderWrappingMode borderWrapModeX, BorderWrappingMode borderWrapModeY) + { + var processor = new GaussianSharpenProcessor(sigma, borderWrapModeX, borderWrapModeY); + return source.ApplyProcessor(processor, rectangle); + } } } diff --git a/src/ImageSharp/Processing/Extensions/Transforms/PadExtensions.cs b/src/ImageSharp/Processing/Extensions/Transforms/PadExtensions.cs index 8c3edcadf..5e9e42032 100644 --- a/src/ImageSharp/Processing/Extensions/Transforms/PadExtensions.cs +++ b/src/ImageSharp/Processing/Extensions/Transforms/PadExtensions.cs @@ -1,6 +1,8 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using System; + namespace SixLabors.ImageSharp.Processing { /// @@ -29,14 +31,17 @@ namespace SixLabors.ImageSharp.Processing /// The to allow chaining of operations. public static IImageProcessingContext Pad(this IImageProcessingContext source, int width, int height, Color color) { + Size size = source.GetCurrentSize(); var options = new ResizeOptions { - Size = new Size(width, height), + // Prevent downsizing. + Size = new Size(Math.Max(width, size.Width), Math.Max(height, size.Height)), Mode = ResizeMode.BoxPad, Sampler = KnownResamplers.NearestNeighbor, + PadColor = color }; - return color.Equals(default) ? source.Resize(options) : source.Resize(options).BackgroundColor(color); + return source.Resize(options); } } } diff --git a/src/ImageSharp/Processing/Processors/Convolution/BorderWrappingMode.cs b/src/ImageSharp/Processing/Processors/Convolution/BorderWrappingMode.cs new file mode 100644 index 000000000..e835fc748 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Convolution/BorderWrappingMode.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution +{ + /// + /// Wrapping mode for the border pixels in convolution processing. + /// + public enum BorderWrappingMode : byte + { + /// Repeat the border pixel value: aaaaaa|abcdefgh|hhhhhhh + Repeat = 0, + + /// Take values from the opposite edge: cdefgh|abcdefgh|abcdefg + Wrap = 1, + + /// Mirror the last few border values: fedcba|abcdefgh|hgfedcb + /// This Mode is similar to , but here the very border pixel is repeated. + Mirror = 2, + + /// Bounce off the border: fedcb|abcdefgh|gfedcb + /// This Mode is similar to , but here the very border pixel is not repeated. + Bounce = 3 + } +} diff --git a/src/ImageSharp/Processing/Processors/Convolution/BoxBlurProcessor.cs b/src/ImageSharp/Processing/Processors/Convolution/BoxBlurProcessor.cs index da6b96718..a622739fd 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/BoxBlurProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/BoxBlurProcessor.cs @@ -21,9 +21,24 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// /// The 'radius' value representing the size of the area to sample. /// - public BoxBlurProcessor(int radius) + /// The to use when mapping the pixels outside of the border, in X direction. + /// The to use when mapping the pixels outside of the border, in Y direction. + public BoxBlurProcessor(int radius, BorderWrappingMode borderWrapModeX, BorderWrappingMode borderWrapModeY) { this.Radius = radius; + this.BorderWrapModeX = borderWrapModeX; + this.BorderWrapModeY = borderWrapModeY; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The 'radius' value representing the size of the area to sample. + /// + public BoxBlurProcessor(int radius) + : this(radius, BorderWrappingMode.Repeat, BorderWrappingMode.Repeat) + { } /// @@ -39,9 +54,19 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// public int Radius { get; } + /// + /// Gets the to use when mapping the pixels outside of the border, in X direction. + /// + public BorderWrappingMode BorderWrapModeX { get; } + + /// + /// Gets the to use when mapping the pixels outside of the border, in Y direction. + /// + public BorderWrappingMode BorderWrapModeY { get; } + /// public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) where TPixel : unmanaged, IPixel - => new BoxBlurProcessor(configuration, this, source, sourceRectangle); + => new BoxBlurProcessor(configuration, this, source, sourceRectangle, this.BorderWrapModeX, this.BorderWrapModeY); } } diff --git a/src/ImageSharp/Processing/Processors/Convolution/BoxBlurProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/BoxBlurProcessor{TPixel}.cs index 5beadb0ce..ceebdf15a 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/BoxBlurProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/BoxBlurProcessor{TPixel}.cs @@ -27,15 +27,49 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution this.Kernel = CreateBoxKernel(kernelSize); } + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The defining the processor parameters. + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + /// The to use when mapping the pixels outside of the border, in X direction. + /// The to use when mapping the pixels outside of the border, in Y direction. + public BoxBlurProcessor( + Configuration configuration, + BoxBlurProcessor definition, + Image source, + Rectangle sourceRectangle, + BorderWrappingMode borderWrapModeX, + BorderWrappingMode borderWrapModeY) + : base(configuration, source, sourceRectangle) + { + int kernelSize = (definition.Radius * 2) + 1; + this.Kernel = CreateBoxKernel(kernelSize); + this.BorderWrapModeX = borderWrapModeX; + this.BorderWrapModeY = borderWrapModeY; + } + /// /// Gets the 1D convolution kernel. /// public float[] Kernel { get; } + /// + /// Gets the to use when mapping the pixels outside of the border, in X direction. + /// + public BorderWrappingMode BorderWrapModeX { get; } + + /// + /// Gets the to use when mapping the pixels outside of the border, in Y direction. + /// + public BorderWrappingMode BorderWrapModeY { get; } + /// protected override void OnFrameApply(ImageFrame source) { - using var processor = new Convolution2PassProcessor(this.Configuration, this.Kernel, false, this.Source, this.SourceRectangle); + using var processor = new Convolution2PassProcessor(this.Configuration, this.Kernel, false, this.Source, this.SourceRectangle, this.BorderWrapModeX, this.BorderWrapModeY); processor.Apply(source); } diff --git a/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs index fa58422dc..2fc0a5fe8 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs @@ -26,16 +26,22 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// Whether the convolution filter is applied to alpha as well as the color channels. /// The source for the current processor instance. /// The source area to process for the current processor instance. + /// The to use when mapping the pixels outside of the border, in X direction. + /// The to use when mapping the pixels outside of the border, in Y direction. public Convolution2PassProcessor( Configuration configuration, float[] kernel, bool preserveAlpha, Image source, - Rectangle sourceRectangle) + Rectangle sourceRectangle, + BorderWrappingMode borderWrapModeX, + BorderWrappingMode borderWrapModeY) : base(configuration, source, sourceRectangle) { this.Kernel = kernel; this.PreserveAlpha = preserveAlpha; + this.BorderWrapModeX = borderWrapModeX; + this.BorderWrapModeY = borderWrapModeY; } /// @@ -48,6 +54,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// public bool PreserveAlpha { get; } + /// + /// Gets the to use when mapping the pixels outside of the border, in X direction. + /// + public BorderWrappingMode BorderWrapModeX { get; } + + /// + /// Gets the to use when mapping the pixels outside of the border, in Y direction. + /// + public BorderWrappingMode BorderWrapModeY { get; } + /// protected override void OnFrameApply(ImageFrame source) { @@ -63,7 +79,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution // the two 1D kernels represent, and reuse it across both convolution steps, like in the bokeh blur. using var mapXY = new KernelSamplingMap(this.Configuration.MemoryAllocator); - mapXY.BuildSamplingOffsetMap(this.Kernel.Length, this.Kernel.Length, interest); + mapXY.BuildSamplingOffsetMap(this.Kernel.Length, this.Kernel.Length, interest, this.BorderWrapModeX, this.BorderWrapModeY); // Horizontal convolution var horizontalOperation = new HorizontalConvolutionRowOperation( diff --git a/src/ImageSharp/Processing/Processors/Convolution/GaussianBlurProcessor.cs b/src/ImageSharp/Processing/Processors/Convolution/GaussianBlurProcessor.cs index 1fa65b62c..3af9791dc 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/GaussianBlurProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/GaussianBlurProcessor.cs @@ -32,6 +32,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution { } + /// + /// Initializes a new instance of the class. + /// + /// The 'sigma' value representing the weight of the blur. + /// The to use when mapping the pixels outside of the border, in X direction. + /// The to use when mapping the pixels outside of the border, in Y direction. + public GaussianBlurProcessor(float sigma, BorderWrappingMode borderWrapModeX, BorderWrappingMode borderWrapModeY) + : this(sigma, ConvolutionProcessorHelpers.GetDefaultGaussianRadius(sigma), borderWrapModeX, borderWrapModeY) + { + } + /// /// Initializes a new instance of the class. /// @@ -54,9 +65,32 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// This should be at least twice the sigma value. /// public GaussianBlurProcessor(float sigma, int radius) + : this(sigma, radius, BorderWrappingMode.Repeat, BorderWrappingMode.Repeat) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The 'sigma' value representing the weight of the blur. + /// + /// + /// The 'radius' value representing the size of the area to sample. + /// This should be at least twice the sigma value. + /// + /// + /// The to use when mapping the pixels outside of the border, in X direction. + /// + /// + /// The to use when mapping the pixels outside of the border, in Y direction. + /// + public GaussianBlurProcessor(float sigma, int radius, BorderWrappingMode borderWrapModeX, BorderWrappingMode borderWrapModeY) { this.Sigma = sigma; this.Radius = radius; + this.BorderWrapModeX = borderWrapModeX; + this.BorderWrapModeY = borderWrapModeY; } /// @@ -69,9 +103,19 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// public int Radius { get; } + /// + /// Gets the to use when mapping the pixels outside of the border, in X direction. + /// + public BorderWrappingMode BorderWrapModeX { get; } + + /// + /// Gets the to use when mapping the pixels outside of the border, in Y direction. + /// + public BorderWrappingMode BorderWrapModeY { get; } + /// public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) where TPixel : unmanaged, IPixel - => new GaussianBlurProcessor(configuration, this, source, sourceRectangle); + => new GaussianBlurProcessor(configuration, this, source, sourceRectangle, this.BorderWrapModeX, this.BorderWrapModeY); } } diff --git a/src/ImageSharp/Processing/Processors/Convolution/GaussianBlurProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/GaussianBlurProcessor{TPixel}.cs index 4ade01f91..16b05b8bb 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/GaussianBlurProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/GaussianBlurProcessor{TPixel}.cs @@ -30,15 +30,49 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution this.Kernel = ConvolutionProcessorHelpers.CreateGaussianBlurKernel(kernelSize, definition.Sigma); } + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The defining the processor parameters. + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + /// The to use when mapping the pixels outside of the border, in X direction. + /// The to use when mapping the pixels outside of the border, in Y direction. + public GaussianBlurProcessor( + Configuration configuration, + GaussianBlurProcessor definition, + Image source, + Rectangle sourceRectangle, + BorderWrappingMode borderWrapModeX, + BorderWrappingMode borderWrapModeY) + : base(configuration, source, sourceRectangle) + { + int kernelSize = (definition.Radius * 2) + 1; + this.Kernel = ConvolutionProcessorHelpers.CreateGaussianBlurKernel(kernelSize, definition.Sigma); + this.BorderWrapModeX = borderWrapModeX; + this.BorderWrapModeY = borderWrapModeY; + } + /// /// Gets the 1D convolution kernel. /// public float[] Kernel { get; } + /// + /// Gets the to use when mapping the pixels outside of the border, in X direction. + /// + public BorderWrappingMode BorderWrapModeX { get; } + + /// + /// Gets the to use when mapping the pixels outside of the border, in Y direction. + /// + public BorderWrappingMode BorderWrapModeY { get; } + /// protected override void OnFrameApply(ImageFrame source) { - using var processor = new Convolution2PassProcessor(this.Configuration, this.Kernel, false, this.Source, this.SourceRectangle); + using var processor = new Convolution2PassProcessor(this.Configuration, this.Kernel, false, this.Source, this.SourceRectangle, this.BorderWrapModeX, this.BorderWrapModeY); processor.Apply(source); } diff --git a/src/ImageSharp/Processing/Processors/Convolution/GaussianSharpenProcessor.cs b/src/ImageSharp/Processing/Processors/Convolution/GaussianSharpenProcessor.cs index 7e1f02906..98c897c21 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/GaussianSharpenProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/GaussianSharpenProcessor.cs @@ -32,6 +32,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution { } + /// + /// Initializes a new instance of the class. + /// + /// The 'sigma' value representing the weight of the blur. + /// The to use when mapping the pixels outside of the border, in X direction. + /// The to use when mapping the pixels outside of the border, in Y direction. + public GaussianSharpenProcessor(float sigma, BorderWrappingMode borderWrapModeX, BorderWrappingMode borderWrapModeY) + : this(sigma, ConvolutionProcessorHelpers.GetDefaultGaussianRadius(sigma), borderWrapModeX, borderWrapModeY) + { + } + /// /// Initializes a new instance of the class. /// @@ -54,9 +65,32 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// This should be at least twice the sigma value. /// public GaussianSharpenProcessor(float sigma, int radius) + : this(sigma, radius, BorderWrappingMode.Repeat, BorderWrappingMode.Repeat) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The 'sigma' value representing the weight of the blur. + /// + /// + /// The 'radius' value representing the size of the area to sample. + /// This should be at least twice the sigma value. + /// + /// + /// The to use when mapping the pixels outside of the border, in X direction. + /// + /// + /// The to use when mapping the pixels outside of the border, in Y direction. + /// + public GaussianSharpenProcessor(float sigma, int radius, BorderWrappingMode borderWrapModeX, BorderWrappingMode borderWrapModeY) { this.Sigma = sigma; this.Radius = radius; + this.BorderWrapModeX = borderWrapModeX; + this.BorderWrapModeY = borderWrapModeY; } /// @@ -69,9 +103,19 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// public int Radius { get; } + /// + /// Gets the to use when mapping the pixels outside of the border, in X direction. + /// + public BorderWrappingMode BorderWrapModeX { get; } + + /// + /// Gets the to use when mapping the pixels outside of the border, in Y direction. + /// + public BorderWrappingMode BorderWrapModeY { get; } + /// public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) where TPixel : unmanaged, IPixel - => new GaussianSharpenProcessor(configuration, this, source, sourceRectangle); + => new GaussianSharpenProcessor(configuration, this, source, sourceRectangle, this.BorderWrapModeX, this.BorderWrapModeY); } } diff --git a/src/ImageSharp/Processing/Processors/Convolution/GaussianSharpenProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/GaussianSharpenProcessor{TPixel}.cs index 73aaaec18..bddaab233 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/GaussianSharpenProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/GaussianSharpenProcessor{TPixel}.cs @@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// Initializes a new instance of the class. /// /// The configuration which allows altering default behaviour or extending the library. - /// The defining the processor parameters. + /// The defining the processor parameters. /// The source for the current processor instance. /// The source area to process for the current processor instance. public GaussianSharpenProcessor( @@ -24,10 +24,32 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution GaussianSharpenProcessor definition, Image source, Rectangle sourceRectangle) + : this(configuration, definition, source, sourceRectangle, BorderWrappingMode.Repeat, BorderWrappingMode.Repeat) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The defining the processor parameters. + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + /// The to use when mapping the pixels outside of the border, in X direction. + /// The to use when mapping the pixels outside of the border, in Y direction. + public GaussianSharpenProcessor( + Configuration configuration, + GaussianSharpenProcessor definition, + Image source, + Rectangle sourceRectangle, + BorderWrappingMode borderWrapModeX, + BorderWrappingMode borderWrapModeY) : base(configuration, source, sourceRectangle) { int kernelSize = (definition.Radius * 2) + 1; this.Kernel = ConvolutionProcessorHelpers.CreateGaussianSharpenKernel(kernelSize, definition.Sigma); + this.BorderWrapModeX = borderWrapModeX; + this.BorderWrapModeY = borderWrapModeY; } /// @@ -35,10 +57,20 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// public float[] Kernel { get; } + /// + /// Gets the to use when mapping the pixels outside of the border, in X direction. + /// + public BorderWrappingMode BorderWrapModeX { get; } + + /// + /// Gets the to use when mapping the pixels outside of the border, in Y direction. + /// + public BorderWrappingMode BorderWrapModeY { get; } + /// protected override void OnFrameApply(ImageFrame source) { - using var processor = new Convolution2PassProcessor(this.Configuration, this.Kernel, false, this.Source, this.SourceRectangle); + using var processor = new Convolution2PassProcessor(this.Configuration, this.Kernel, false, this.Source, this.SourceRectangle, this.BorderWrapModeX, this.BorderWrapModeY); processor.Apply(source); } diff --git a/src/ImageSharp/Processing/Processors/Convolution/KernelSamplingMap.cs b/src/ImageSharp/Processing/Processors/Convolution/KernelSamplingMap.cs index 904b599f7..98a4ca357 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/KernelSamplingMap.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/KernelSamplingMap.cs @@ -31,7 +31,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// The convolution kernel. /// The source bounds. public void BuildSamplingOffsetMap(DenseMatrix kernel, Rectangle bounds) - => this.BuildSamplingOffsetMap(kernel.Rows, kernel.Columns, bounds); + => this.BuildSamplingOffsetMap(kernel.Rows, kernel.Columns, bounds, BorderWrappingMode.Repeat, BorderWrappingMode.Repeat); /// /// Builds a map of the sampling offsets for the kernel clamped by the given bounds. @@ -40,6 +40,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// The width (number of columns) of the convolution kernel to use. /// The source bounds. public void BuildSamplingOffsetMap(int kernelHeight, int kernelWidth, Rectangle bounds) + => this.BuildSamplingOffsetMap(kernelHeight, kernelWidth, bounds, BorderWrappingMode.Repeat, BorderWrappingMode.Repeat); + + /// + /// Builds a map of the sampling offsets for the kernel clamped by the given bounds. + /// + /// The height (number of rows) of the convolution kernel to use. + /// The width (number of columns) of the convolution kernel to use. + /// The source bounds. + /// The wrapping mode on the horizontal borders. + /// The wrapping mode on the vertical borders. + public void BuildSamplingOffsetMap(int kernelHeight, int kernelWidth, Rectangle bounds, BorderWrappingMode xBorderMode, BorderWrappingMode yBorderMode) { this.yOffsets = this.allocator.Allocate(bounds.Height * kernelHeight); this.xOffsets = this.allocator.Allocate(bounds.Width * kernelWidth); @@ -49,43 +60,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution int minX = bounds.X; int maxX = bounds.Right - 1; - int radiusY = kernelHeight >> 1; - int radiusX = kernelWidth >> 1; - - // Calculate the y and x sampling offsets clamped to the given rectangle. - // While this isn't a hotpath we still dip into unsafe to avoid the span bounds - // checks as the can potentially be looping over large arrays. - Span ySpan = this.yOffsets.GetSpan(); - ref int ySpanBase = ref MemoryMarshal.GetReference(ySpan); - for (int row = 0; row < bounds.Height; row++) - { - int rowBase = row * kernelHeight; - for (int y = 0; y < kernelHeight; y++) - { - Unsafe.Add(ref ySpanBase, rowBase + y) = row + y + minY - radiusY; - } - } - - if (kernelHeight > 1) - { - Numerics.Clamp(ySpan, minY, maxY); - } - - Span xSpan = this.xOffsets.GetSpan(); - ref int xSpanBase = ref MemoryMarshal.GetReference(xSpan); - for (int column = 0; column < bounds.Width; column++) - { - int columnBase = column * kernelWidth; - for (int x = 0; x < kernelWidth; x++) - { - Unsafe.Add(ref xSpanBase, columnBase + x) = column + x + minX - radiusX; - } - } - - if (kernelWidth > 1) - { - Numerics.Clamp(xSpan, minX, maxX); - } + this.BuildOffsets(this.yOffsets, bounds.Height, kernelHeight, minY, maxY, yBorderMode); + this.BuildOffsets(this.xOffsets, bounds.Width, kernelWidth, minX, maxX, xBorderMode); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -105,5 +81,105 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution this.isDisposed = true; } } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void BuildOffsets(IMemoryOwner offsets, int boundsSize, int kernelSize, int min, int max, BorderWrappingMode borderMode) + { + int radius = kernelSize >> 1; + Span span = offsets.GetSpan(); + ref int spanBase = ref MemoryMarshal.GetReference(span); + for (int chunk = 0; chunk < boundsSize; chunk++) + { + int chunkBase = chunk * kernelSize; + for (int i = 0; i < kernelSize; i++) + { + Unsafe.Add(ref spanBase, chunkBase + i) = chunk + i + min - radius; + } + } + + this.CorrectBorder(span, kernelSize, min, max, borderMode); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CorrectBorder(Span span, int kernelSize, int min, int max, BorderWrappingMode borderMode) + { + var affectedSize = (kernelSize >> 1) * kernelSize; + ref int spanBase = ref MemoryMarshal.GetReference(span); + if (affectedSize > 0) + { + switch (borderMode) + { + case BorderWrappingMode.Repeat: + Numerics.Clamp(span.Slice(0, affectedSize), min, max); + Numerics.Clamp(span.Slice(span.Length - affectedSize), min, max); + break; + case BorderWrappingMode.Mirror: + var min2dec = min + min - 1; + for (int i = 0; i < affectedSize; i++) + { + var value = span[i]; + if (value < min) + { + span[i] = min2dec - value; + } + } + + var max2inc = max + max + 1; + for (int i = span.Length - affectedSize; i < span.Length; i++) + { + var value = span[i]; + if (value > max) + { + span[i] = max2inc - value; + } + } + + break; + case BorderWrappingMode.Bounce: + var min2 = min + min; + for (int i = 0; i < affectedSize; i++) + { + var value = span[i]; + if (value < min) + { + span[i] = min2 - value; + } + } + + var max2 = max + max; + for (int i = span.Length - affectedSize; i < span.Length; i++) + { + var value = span[i]; + if (value > max) + { + span[i] = max2 - value; + } + } + + break; + case BorderWrappingMode.Wrap: + var diff = max - min + 1; + for (int i = 0; i < affectedSize; i++) + { + var value = span[i]; + if (value < min) + { + span[i] = diff + value; + } + } + + for (int i = span.Length - affectedSize; i < span.Length; i++) + { + var value = span[i]; + if (value > max) + { + span[i] = value - diff; + } + } + + break; + } + } + } } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs index cc5329952..5c5344ab8 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs @@ -176,9 +176,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization Rgba32 rgba = default; color.ToRgba32(ref rgba); - int r = rgba.R >> (8 - IndexBits); - int g = rgba.G >> (8 - IndexBits); - int b = rgba.B >> (8 - IndexBits); + const int shift = 8 - IndexBits; + int r = rgba.R >> shift; + int g = rgba.G >> shift; + int b = rgba.B >> shift; int a = rgba.A >> (8 - IndexAlphaBits); ReadOnlySpan tagSpan = this.tagsOwner.GetSpan(); @@ -413,6 +414,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization Span momentSpan = this.momentsOwner.GetSpan(); Span volumeSpan = volume.GetSpan(); Span areaSpan = area.GetSpan(); + const int indexBits2 = IndexBits * 2; + const int indexAndAlphaBits = IndexBits + IndexAlphaBits; + const int indexBitsAndAlphaBits1 = IndexBits + IndexAlphaBits + 1; int baseIndex = GetPaletteIndex(1, 0, 0, 0); for (int r = 1; r < IndexCount; r++) @@ -421,9 +425,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization // immediate outer loop. See https://github.com/dotnet/runtime/issues/61420 // To ensure the calculation doesn't happen repeatedly, hoist some of the calculations // in the form of ind1* manually. - int ind1R = (r << ((IndexBits * 2) + IndexAlphaBits)) + - (r << (IndexBits + IndexAlphaBits + 1)) + - (r << (IndexBits * 2)) + + int ind1R = (r << (indexBits2 + IndexAlphaBits)) + + (r << indexBitsAndAlphaBits1) + + (r << indexBits2) + (r << (IndexBits + 1)) + r; @@ -432,7 +436,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization for (int g = 1; g < IndexCount; g++) { int ind1G = ind1R + - (g << (IndexBits + IndexAlphaBits)) + + (g << indexAndAlphaBits) + (g << IndexBits) + g; int r_g = r + g; @@ -446,7 +450,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization b; Moment line = default; - + int bIndexAlphaOffset = b * IndexAlphaCount; for (int a = 1; a < IndexAlphaCount; a++) { int ind1 = ind1B + a; @@ -455,7 +459,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization areaSpan[a] += line; - int inv = (b * IndexAlphaCount) + a; + int inv = bIndexAlphaOffset + a; volumeSpan[inv] += areaSpan[a]; int ind2 = ind1 - baseIndex; diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs index f6fadca33..f74cf5949 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs @@ -187,12 +187,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms && RuntimeEnvironment.IsNetCore) { // There's something wrong with the JIT in .NET Core 3.1 on certain - // MacOSX machines so we have to use different pipelines. + // macOS machines so we have to use different pipelines. // It's: // - Not reproducable locally // - Doesn't seem to be triggered by the bulk Numerics.UnPremultiply method but by caller. // https://github.com/SixLabors/ImageSharp/pull/1591 - this.InvokeMacOSX(in rows, span); + this.InvokeMacOS(in rows, span); return; } @@ -259,7 +259,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms [ExcludeFromCodeCoverage] [MethodImpl(InliningOptions.ShortMethod)] - private void InvokeMacOSX(in RowInterval rows, Span span) + private void InvokeMacOS(in RowInterval rows, Span span) { Matrix3x2 matrix = this.matrix; TResampler sampler = this.sampler; diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/AutoOrientProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/AutoOrientProcessor{TPixel}.cs index 6c5219b3a..c9f9d4327 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Linear/AutoOrientProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/AutoOrientProcessor{TPixel}.cs @@ -28,42 +28,42 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// protected override void BeforeImageApply() { - OrientationMode orientation = GetExifOrientation(this.Source); + ushort orientation = GetExifOrientation(this.Source); Size size = this.SourceRectangle.Size; switch (orientation) { - case OrientationMode.TopRight: + case ExifOrientationMode.TopRight: new FlipProcessor(FlipMode.Horizontal).Execute(this.Configuration, this.Source, this.SourceRectangle); break; - case OrientationMode.BottomRight: + case ExifOrientationMode.BottomRight: new RotateProcessor((int)RotateMode.Rotate180, size).Execute(this.Configuration, this.Source, this.SourceRectangle); break; - case OrientationMode.BottomLeft: + case ExifOrientationMode.BottomLeft: new FlipProcessor(FlipMode.Vertical).Execute(this.Configuration, this.Source, this.SourceRectangle); break; - case OrientationMode.LeftTop: + case ExifOrientationMode.LeftTop: new RotateProcessor((int)RotateMode.Rotate90, size).Execute(this.Configuration, this.Source, this.SourceRectangle); new FlipProcessor(FlipMode.Horizontal).Execute(this.Configuration, this.Source, this.SourceRectangle); break; - case OrientationMode.RightTop: + case ExifOrientationMode.RightTop: new RotateProcessor((int)RotateMode.Rotate90, size).Execute(this.Configuration, this.Source, this.SourceRectangle); break; - case OrientationMode.RightBottom: + case ExifOrientationMode.RightBottom: new FlipProcessor(FlipMode.Vertical).Execute(this.Configuration, this.Source, this.SourceRectangle); new RotateProcessor((int)RotateMode.Rotate270, size).Execute(this.Configuration, this.Source, this.SourceRectangle); break; - case OrientationMode.LeftBottom: + case ExifOrientationMode.LeftBottom: new RotateProcessor((int)RotateMode.Rotate270, size).Execute(this.Configuration, this.Source, this.SourceRectangle); break; - case OrientationMode.Unknown: - case OrientationMode.TopLeft: + case ExifOrientationMode.Unknown: + case ExifOrientationMode.TopLeft: default: break; } @@ -81,32 +81,32 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// Returns the current EXIF orientation /// /// The image to auto rotate. - /// The - private static OrientationMode GetExifOrientation(Image source) + /// The + private static ushort GetExifOrientation(Image source) { if (source.Metadata.ExifProfile is null) { - return OrientationMode.Unknown; + return ExifOrientationMode.Unknown; } IExifValue value = source.Metadata.ExifProfile.GetValue(ExifTag.Orientation); if (value is null) { - return OrientationMode.Unknown; + return ExifOrientationMode.Unknown; } - OrientationMode orientation; + ushort orientation; if (value.DataType == ExifDataType.Short) { - orientation = (OrientationMode)value.Value; + orientation = value.Value; } else { - orientation = (OrientationMode)Convert.ToUInt16(value.Value); + orientation = Convert.ToUInt16(value.Value); source.Metadata.ExifProfile.RemoveValue(ExifTag.Orientation); } - source.Metadata.ExifProfile.SetValue(ExifTag.Orientation, (ushort)OrientationMode.TopLeft); + source.Metadata.ExifProfile.SetValue(ExifTag.Orientation, ExifOrientationMode.TopLeft); return orientation; } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs index 26b970d8b..9bb9c113d 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs @@ -186,12 +186,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms && RuntimeEnvironment.IsNetCore) { // There's something wrong with the JIT in .NET Core 3.1 on certain - // MacOSX machines so we have to use different pipelines. + // macOS machines so we have to use different pipelines. // It's: // - Not reproducable locally // - Doesn't seem to be triggered by the bulk Numerics.UnPremultiply method but by caller. // https://github.com/SixLabors/ImageSharp/pull/1591 - this.InvokeMacOSX(in rows, span); + this.InvokeMacOS(in rows, span); return; } @@ -258,7 +258,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms [ExcludeFromCodeCoverage] [MethodImpl(InliningOptions.ShortMethod)] - public void InvokeMacOSX(in RowInterval rows, Span span) + public void InvokeMacOS(in RowInterval rows, Span span) { Matrix4x4 matrix = this.matrix; TResampler sampler = this.sampler; diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs index c9dda5f6b..f4bda8bf4 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs @@ -101,7 +101,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// Returns a for an index value between 0 and DestinationSize - 1. /// [MethodImpl(InliningOptions.ShortMethod)] - internal ref ResizeKernel GetKernel(int destIdx) => ref this.kernels[destIdx]; + internal ref ResizeKernel GetKernel(nint destIdx) => ref this.kernels[destIdx]; /// /// Computes the weights to apply at each pixel when resizing. diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs index 3d6900683..ef6a15fc9 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs @@ -21,19 +21,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms (Size size, Rectangle rectangle) = ResizeHelper.CalculateTargetLocationAndBounds(sourceSize, options); - this.Sampler = options.Sampler; + this.Options = options; this.DestinationWidth = size.Width; this.DestinationHeight = size.Height; this.DestinationRectangle = rectangle; - this.Compand = options.Compand; - this.PremultiplyAlpha = options.PremultiplyAlpha; } - /// - /// Gets the sampler to perform the resize operation. - /// - public IResampler Sampler { get; } - /// /// Gets the destination width. /// @@ -50,14 +43,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms public Rectangle DestinationRectangle { get; } /// - /// Gets a value indicating whether to compress or expand individual pixel color values on processing. - /// - public bool Compand { get; } - - /// - /// Gets a value indicating whether to premultiply the alpha (if it exists) during the resize operation. + /// Gets the resize options. /// - public bool PremultiplyAlpha { get; } + public ResizeOptions Options { get; } /// public override ICloningImageProcessor CreatePixelSpecificCloningProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor{TPixel}.cs index b486e4225..b34675896 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor{TPixel}.cs @@ -4,7 +4,6 @@ using System; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -17,12 +16,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms internal class ResizeProcessor : TransformProcessor, IResamplingTransformImageProcessor where TPixel : unmanaged, IPixel { + private readonly ResizeOptions options; private readonly int destinationWidth; private readonly int destinationHeight; private readonly IResampler resampler; private readonly Rectangle destinationRectangle; - private readonly bool compand; - private readonly bool premultiplyAlpha; private Image destination; public ResizeProcessor(Configuration configuration, ResizeProcessor definition, Image source, Rectangle sourceRectangle) @@ -31,13 +29,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms this.destinationWidth = definition.DestinationWidth; this.destinationHeight = definition.DestinationHeight; this.destinationRectangle = definition.DestinationRectangle; - this.resampler = definition.Sampler; - this.premultiplyAlpha = definition.PremultiplyAlpha; - this.compand = definition.Compand; + this.options = definition.Options; + this.resampler = definition.Options.Sampler; } /// - protected override Size GetDestinationSize() => new Size(this.destinationWidth, this.destinationHeight); + protected override Size GetDestinationSize() => new(this.destinationWidth, this.destinationHeight); /// protected override void BeforeImageApply(Image destination) @@ -62,8 +59,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms Image destination = this.destination; Rectangle sourceRectangle = this.SourceRectangle; Rectangle destinationRectangle = this.destinationRectangle; - bool compand = this.compand; - bool premultiplyAlpha = this.premultiplyAlpha; + bool compand = this.options.Compand; + bool premultiplyAlpha = this.options.PremultiplyAlpha; + TPixel fillColor = this.options.PadColor.ToPixel(); + bool shouldFill = (this.options.Mode == ResizeMode.BoxPad || this.options.Mode == ResizeMode.Pad) + && this.options.PadColor != default; // Handle resize dimensions identical to the original if (source.Width == destination.Width @@ -91,6 +91,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms ImageFrame sourceFrame = source.Frames[i]; ImageFrame destinationFrame = destination.Frames[i]; + if (shouldFill) + { + destinationFrame.Clear(fillColor); + } + ApplyNNResizeFrameTransform( configuration, sourceFrame, @@ -123,6 +128,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms ImageFrame sourceFrame = source.Frames[i]; ImageFrame destinationFrame = destination.Frames[i]; + if (shouldFill) + { + destinationFrame.Clear(fillColor); + } + ApplyResizeFrameTransform( configuration, sourceFrame, @@ -199,21 +209,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms // To reintroduce parallel processing, we would launch multiple workers // for different row intervals of the image. - using (var worker = new ResizeWorker( + using var worker = new ResizeWorker( configuration, sourceRegion, conversionModifiers, horizontalKernelMap, verticalKernelMap, - destination.Width, interest, - destinationRectangle.Location)) - { - worker.Initialize(); + destinationRectangle.Location); + worker.Initialize(); - var workingInterval = new RowInterval(interest.Top, interest.Bottom); - worker.FillDestinationPixels(workingInterval, destination.PixelBuffer); - } + var workingInterval = new RowInterval(interest.Top, interest.Bottom); + worker.FillDestinationPixels(workingInterval, destination.PixelBuffer); } private readonly struct NNRowOperation : IRowOperation diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs index 90cbf8bda..45f35b7d9 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs @@ -39,8 +39,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms private readonly ResizeKernelMap verticalKernelMap; - private readonly int destWidth; - private readonly Rectangle targetWorkingRect; private readonly Point targetOrigin; @@ -57,7 +55,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms PixelConversionModifiers conversionModifiers, ResizeKernelMap horizontalKernelMap, ResizeKernelMap verticalKernelMap, - int destWidth, Rectangle targetWorkingRect, Point targetOrigin) { @@ -67,7 +64,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms this.conversionModifiers = conversionModifiers; this.horizontalKernelMap = horizontalKernelMap; this.verticalKernelMap = verticalKernelMap; - this.destWidth = destWidth; this.targetWorkingRect = targetWorkingRect; this.targetOrigin = targetOrigin; @@ -80,19 +76,19 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms int numberOfWindowBands = ResizeHelper.CalculateResizeWorkerHeightInWindowBands( this.windowBandHeight, - destWidth, + targetWorkingRect.Width, workingBufferLimitHintInBytes); this.workerHeight = Math.Min(this.sourceRectangle.Height, numberOfWindowBands * this.windowBandHeight); this.transposedFirstPassBuffer = configuration.MemoryAllocator.Allocate2D( this.workerHeight, - destWidth, + targetWorkingRect.Width, preferContiguosImageBuffers: true, options: AllocationOptions.Clean); this.tempRowBuffer = configuration.MemoryAllocator.Allocate(this.sourceRectangle.Width); - this.tempColumnBuffer = configuration.MemoryAllocator.Allocate(destWidth); + this.tempColumnBuffer = configuration.MemoryAllocator.Allocate(targetWorkingRect.Width); this.currentWindow = new RowInterval(0, this.workerHeight); } @@ -118,6 +114,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms // When creating transposedFirstPassBuffer, we made sure it's contiguous: Span transposedFirstPassBufferSpan = this.transposedFirstPassBuffer.DangerousGetSingleSpan(); + int left = this.targetWorkingRect.Left; + int right = this.targetWorkingRect.Right; + int width = this.targetWorkingRect.Width; for (int y = rowInterval.Min; y < rowInterval.Max; y++) { // Ensure offsets are normalized for cropping and padding. @@ -131,9 +130,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms ref Vector4 tempRowBase = ref MemoryMarshal.GetReference(tempColSpan); int top = kernel.StartIndex - this.currentWindow.Min; + ref Vector4 fpBase = ref transposedFirstPassBufferSpan[top]; - for (int x = 0; x < this.destWidth; x++) + for (nint x = 0; x < (right - left); x++) { ref Vector4 firstPassColumnBase = ref Unsafe.Add(ref fpBase, x * this.workerHeight); @@ -141,7 +141,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms Unsafe.Add(ref tempRowBase, x) = kernel.ConvolveCore(ref firstPassColumnBase); } - Span targetRowSpan = destination.DangerousGetRowSpan(y); + Span targetRowSpan = destination.DangerousGetRowSpan(y).Slice(left, width); PixelOperations.Instance.FromVector4Destructive(this.configuration, tempColSpan, targetRowSpan, this.conversionModifiers); } @@ -170,6 +170,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms Span tempRowSpan = this.tempRowBuffer.GetSpan(); Span transposedFirstPassBufferSpan = this.transposedFirstPassBuffer.DangerousGetSingleSpan(); + int left = this.targetWorkingRect.Left; + int right = this.targetWorkingRect.Right; + int targetOriginX = this.targetOrigin.X; for (int y = calculationInterval.Min; y < calculationInterval.Max; y++) { Span sourceRow = this.source.DangerousGetRowSpan(y); @@ -184,13 +187,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms // Span firstPassSpan = transposedFirstPassBufferSpan.Slice(y - this.currentWindow.Min); ref Vector4 firstPassBaseRef = ref transposedFirstPassBufferSpan[y - this.currentWindow.Min]; - for (int x = this.targetWorkingRect.Left; x < this.targetWorkingRect.Right; x++) + for (nint x = left, z = 0; x < right; x++, z++) { - ResizeKernel kernel = this.horizontalKernelMap.GetKernel(x - this.targetOrigin.X); + ResizeKernel kernel = this.horizontalKernelMap.GetKernel(x - targetOriginX); // optimization for: // firstPassSpan[x * this.workerHeight] = kernel.Convolve(tempRowSpan); - Unsafe.Add(ref firstPassBaseRef, x * this.workerHeight) = kernel.Convolve(tempRowSpan); + Unsafe.Add(ref firstPassBaseRef, z * this.workerHeight) = kernel.Convolve(tempRowSpan); } } } diff --git a/src/ImageSharp/Processing/ResizeOptions.cs b/src/ImageSharp/Processing/ResizeOptions.cs index 4b31998da..62cf8ab23 100644 --- a/src/ImageSharp/Processing/ResizeOptions.cs +++ b/src/ImageSharp/Processing/ResizeOptions.cs @@ -51,5 +51,10 @@ namespace SixLabors.ImageSharp.Processing /// the alpha (if it exists) during the resize operation. /// public bool PremultiplyAlpha { get; set; } = true; + + /// + /// Gets or sets the color to use as a background when padding an image. + /// + public Color PadColor { get; set; } } } diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpeg.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpeg.cs index 9665ca42d..d64eb15ae 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpeg.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpeg.cs @@ -24,7 +24,7 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg private void GenericBechmark() { this.preloadedImageStream.Position = 0; - using Image img = this.decoder.Decode(Configuration.Default, this.preloadedImageStream); + using Image img = this.decoder.Decode(Configuration.Default, this.preloadedImageStream, default); } [GlobalSetup(Target = nameof(JpegBaselineInterleaved444))] diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs index 9db666c37..450c786ad 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs @@ -39,10 +39,9 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg using var memoryStream = new MemoryStream(this.jpegBytes); using var bufferedStream = new BufferedReadStream(Configuration.Default, memoryStream); - var decoder = new JpegDecoderCore(Configuration.Default, new JpegDecoder { IgnoreMetadata = true }); - var scanDecoder = new HuffmanScanDecoder(bufferedStream, new NoopSpectralConverter(), cancellationToken: default); - decoder.ParseStream(bufferedStream, scanDecoder, cancellationToken: default); - decoder.Dispose(); + using var decoder = new JpegDecoderCore(Configuration.Default, new JpegDecoder { IgnoreMetadata = true }); + var spectralConverter = new NoopSpectralConverter(); + decoder.ParseStream(bufferedStream, spectralConverter, cancellationToken: default); } // We want to test only stream parsing and scan decoding, we don't need to convert spectral data to actual pixels diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/EncodeJpeg.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/EncodeJpeg.cs deleted file mode 100644 index 0e9bed1d9..000000000 --- a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/EncodeJpeg.cs +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -using System.Drawing.Imaging; -using System.IO; -using BenchmarkDotNet.Attributes; -using SixLabors.ImageSharp.Formats.Jpeg; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Tests; -using SDImage = System.Drawing.Image; - -namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg -{ - public class EncodeJpeg - { - [Params(75, 90, 100)] - public int Quality; - - private const string TestImage = TestImages.Jpeg.BenchmarkSuite.Jpeg420Exif_MidSizeYCbCr; - - // System.Drawing - private SDImage bmpDrawing; - private Stream bmpStream; - private ImageCodecInfo jpegCodec; - private EncoderParameters encoderParameters; - - // ImageSharp - private Image bmpCore; - private JpegEncoder encoder420; - private JpegEncoder encoder444; - - private MemoryStream destinationStream; - - [GlobalSetup] - public void ReadImages() - { - if (this.bmpStream == null) - { - this.bmpStream = File.OpenRead(Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImage)); - - this.bmpCore = Image.Load(this.bmpStream); - this.bmpCore.Metadata.ExifProfile = null; - this.encoder420 = new JpegEncoder { Quality = this.Quality, ColorType = JpegColorType.YCbCrRatio420 }; - this.encoder444 = new JpegEncoder { Quality = this.Quality, ColorType = JpegColorType.YCbCrRatio444 }; - - this.bmpStream.Position = 0; - this.bmpDrawing = SDImage.FromStream(this.bmpStream); - this.jpegCodec = GetEncoder(ImageFormat.Jpeg); - this.encoderParameters = new EncoderParameters(1); - - // Quality cast to long is necessary -#pragma warning disable IDE0004 // Remove Unnecessary Cast - this.encoderParameters.Param[0] = new EncoderParameter(Encoder.Quality, (long)this.Quality); -#pragma warning restore IDE0004 // Remove Unnecessary Cast - - this.destinationStream = new MemoryStream(); - } - } - - [GlobalCleanup] - public void Cleanup() - { - this.bmpStream.Dispose(); - this.bmpStream = null; - - this.destinationStream.Dispose(); - this.destinationStream = null; - - this.bmpCore.Dispose(); - this.bmpDrawing.Dispose(); - - this.encoderParameters.Dispose(); - } - - [Benchmark(Baseline = true, Description = "System.Drawing Jpeg 4:2:0")] - public void JpegSystemDrawing() - { - this.bmpDrawing.Save(this.destinationStream, this.jpegCodec, this.encoderParameters); - this.destinationStream.Seek(0, SeekOrigin.Begin); - } - - [Benchmark(Description = "ImageSharp Jpeg 4:2:0")] - public void JpegCore420() - { - this.bmpCore.SaveAsJpeg(this.destinationStream, this.encoder420); - this.destinationStream.Seek(0, SeekOrigin.Begin); - } - - [Benchmark(Description = "ImageSharp Jpeg 4:4:4")] - public void JpegCore444() - { - this.bmpCore.SaveAsJpeg(this.destinationStream, this.encoder444); - this.destinationStream.Seek(0, SeekOrigin.Begin); - } - - // https://docs.microsoft.com/en-us/dotnet/api/system.drawing.imaging.encoderparameter?redirectedfrom=MSDN&view=net-5.0 - private static ImageCodecInfo GetEncoder(ImageFormat format) - { - ImageCodecInfo[] codecs = ImageCodecInfo.GetImageDecoders(); - foreach (ImageCodecInfo codec in codecs) - { - if (codec.FormatID == format.Guid) - { - return codec; - } - } - - return null; - } - } -} - -/* -BenchmarkDotNet=v0.13.0, OS=Windows 10.0.19042 -Intel Core i7-6700K CPU 4.00GHz (Skylake), 1 CPU, 8 logical and 4 physical cores -.NET SDK=6.0.100-preview.3.21202.5 - [Host] : .NET Core 3.1.18 (CoreCLR 4.700.21.35901, CoreFX 4.700.21.36305), X64 RyuJIT - DefaultJob : .NET Core 3.1.18 (CoreCLR 4.700.21.35901, CoreFX 4.700.21.36305), X64 RyuJIT - - -| Method | Quality | Mean | Error | StdDev | Ratio | -|---------------------------- |-------- |---------:|---------:|---------:|------:| -| 'System.Drawing Jpeg 4:2:0' | 75 | 30.04 ms | 0.540 ms | 0.479 ms | 1.00 | -| 'ImageSharp Jpeg 4:2:0' | 75 | 19.32 ms | 0.290 ms | 0.257 ms | 0.64 | -| 'ImageSharp Jpeg 4:4:4' | 75 | 26.76 ms | 0.332 ms | 0.294 ms | 0.89 | -| | | | | | | -| 'System.Drawing Jpeg 4:2:0' | 90 | 32.82 ms | 0.184 ms | 0.163 ms | 1.00 | -| 'ImageSharp Jpeg 4:2:0' | 90 | 25.00 ms | 0.408 ms | 0.361 ms | 0.76 | -| 'ImageSharp Jpeg 4:4:4' | 90 | 31.83 ms | 0.636 ms | 0.595 ms | 0.97 | -| | | | | | | -| 'System.Drawing Jpeg 4:2:0' | 100 | 39.30 ms | 0.359 ms | 0.318 ms | 1.00 | -| 'ImageSharp Jpeg 4:2:0' | 100 | 34.49 ms | 0.265 ms | 0.235 ms | 0.88 | -| 'ImageSharp Jpeg 4:4:4' | 100 | 56.40 ms | 0.565 ms | 0.501 ms | 1.44 | -*/ diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/EncodeJpegComparison.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/EncodeJpegComparison.cs new file mode 100644 index 000000000..2c4686edd --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/EncodeJpegComparison.cs @@ -0,0 +1,112 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Drawing.Imaging; +using System.IO; +using BenchmarkDotNet.Attributes; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests; +using SkiaSharp; +using SDImage = System.Drawing.Image; + +namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg +{ + /// + /// Benchmark for performance comparison between other codecs. + /// + /// + /// This benchmarks tests baseline 4:2:0 chroma sampling path. + /// + public class EncodeJpegComparison + { + // Big enough, 4:4:4 chroma sampling + private const string TestImage = TestImages.Jpeg.Baseline.Calliphora; + + // Change/add parameters for extra benchmarks + [Params(75, 90, 100)] + public int Quality; + + private MemoryStream destinationStream; + + // ImageSharp + private Image imageImageSharp; + private JpegEncoder encoderImageSharp; + + // SkiaSharp + private SKBitmap imageSkiaSharp; + + [GlobalSetup(Target = nameof(BenchmarkImageSharp))] + public void SetupImageSharp() + { + using FileStream imageBinaryStream = File.OpenRead(Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImage)); + + this.imageImageSharp = Image.Load(imageBinaryStream); + this.encoderImageSharp = new JpegEncoder { Quality = this.Quality, ColorType = JpegColorType.YCbCrRatio420 }; + + this.destinationStream = new MemoryStream(); + } + + [GlobalCleanup(Target = nameof(BenchmarkImageSharp))] + public void CleanupImageSharp() + { + this.imageImageSharp.Dispose(); + this.imageImageSharp = null; + + this.destinationStream.Dispose(); + this.destinationStream = null; + } + + [Benchmark(Description = "ImageSharp")] + public void BenchmarkImageSharp() + { + this.imageImageSharp.SaveAsJpeg(this.destinationStream, this.encoderImageSharp); + this.destinationStream.Seek(0, SeekOrigin.Begin); + } + + [GlobalSetup(Target = nameof(BenchmarkSkiaSharp))] + public void SetupSkiaSharp() + { + using FileStream imageBinaryStream = File.OpenRead(Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImage)); + + this.imageSkiaSharp = SKBitmap.Decode(imageBinaryStream); + + this.destinationStream = new MemoryStream(); + } + + [GlobalCleanup(Target = nameof(BenchmarkSkiaSharp))] + public void CleanupSkiaSharp() + { + this.imageSkiaSharp.Dispose(); + this.imageSkiaSharp = null; + + this.destinationStream.Dispose(); + this.destinationStream = null; + } + + [Benchmark(Description = "SkiaSharp")] + public void BenchmarkSkiaSharp() + { + this.imageSkiaSharp.Encode(SKEncodedImageFormat.Jpeg, this.Quality).SaveTo(this.destinationStream); + this.destinationStream.Seek(0, SeekOrigin.Begin); + } + } +} + +/* +BenchmarkDotNet=v0.13.0, OS=Windows 10.0.19044 +Intel Core i7-6700K CPU 4.00GHz (Skylake), 1 CPU, 8 logical and 4 physical cores +.NET SDK=6.0.100-preview.3.21202.5 + [Host] : .NET Core 3.1.21 (CoreCLR 4.700.21.51404, CoreFX 4.700.21.51508), X64 RyuJIT + DefaultJob : .NET Core 3.1.21 (CoreCLR 4.700.21.51404, CoreFX 4.700.21.51508), X64 RyuJIT + + +| Method | Quality | Mean | Error | StdDev | +|----------- |-------- |----------:|----------:|----------:| +| ImageSharp | 75 | 6.820 ms | 0.0374 ms | 0.0312 ms | +| SkiaSharp | 75 | 16.417 ms | 0.3238 ms | 0.4747 ms | +| ImageSharp | 90 | 7.849 ms | 0.1565 ms | 0.3126 ms | +| SkiaSharp | 90 | 16.893 ms | 0.2200 ms | 0.2058 ms | +| ImageSharp | 100 | 11.016 ms | 0.2087 ms | 0.1850 ms | +| SkiaSharp | 100 | 20.410 ms | 0.2583 ms | 0.2290 ms | +*/ diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/EncodeJpegFeatures.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/EncodeJpegFeatures.cs new file mode 100644 index 000000000..83fb556d5 --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/EncodeJpegFeatures.cs @@ -0,0 +1,89 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Collections.Generic; +using System.IO; +using BenchmarkDotNet.Attributes; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests; + +namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg +{ + /// + /// Benchmark for all available encoding features of the Jpeg file type. + /// + /// + /// This benchmark does NOT compare ImageSharp to any other jpeg codecs. + /// + public class EncodeJpegFeatures + { + // Big enough, 4:4:4 chroma sampling + // No metadata + private const string TestImage = TestImages.Jpeg.Baseline.Calliphora; + + public static IEnumerable ColorSpaceValues => + new[] { JpegColorType.Luminance, JpegColorType.Rgb, JpegColorType.YCbCrRatio420, JpegColorType.YCbCrRatio444 }; + + [Params(75, 90, 100)] + public int Quality; + + [ParamsSource(nameof(ColorSpaceValues), Priority = -100)] + public JpegColorType TargetColorSpace; + + private Image bmpCore; + private JpegEncoder encoder; + + private MemoryStream destinationStream; + + [GlobalSetup] + public void Setup() + { + using FileStream imageBinaryStream = File.OpenRead(Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImage)); + this.bmpCore = Image.Load(imageBinaryStream); + this.encoder = new JpegEncoder { Quality = this.Quality, ColorType = this.TargetColorSpace }; + this.destinationStream = new MemoryStream(); + } + + [GlobalCleanup] + public void Cleanup() + { + this.bmpCore.Dispose(); + this.bmpCore = null; + + this.destinationStream.Dispose(); + this.destinationStream = null; + } + + [Benchmark] + public void Benchmark() + { + this.bmpCore.SaveAsJpeg(this.destinationStream, this.encoder); + this.destinationStream.Seek(0, SeekOrigin.Begin); + } + } +} + +/* +BenchmarkDotNet=v0.13.0, OS=Windows 10.0.19044 +Intel Core i7-6700K CPU 4.00GHz (Skylake), 1 CPU, 8 logical and 4 physical cores +.NET SDK=6.0.100-preview.3.21202.5 + [Host] : .NET Core 3.1.21 (CoreCLR 4.700.21.51404, CoreFX 4.700.21.51508), X64 RyuJIT + DefaultJob : .NET Core 3.1.21 (CoreCLR 4.700.21.51404, CoreFX 4.700.21.51508), X64 RyuJIT + + +| Method | TargetColorSpace | Quality | Mean | Error | StdDev | +|---------- |----------------- |-------- |----------:|----------:|----------:| +| Benchmark | Luminance | 75 | 7.055 ms | 0.1411 ms | 0.3297 ms | +| Benchmark | Rgb | 75 | 12.139 ms | 0.0645 ms | 0.0538 ms | +| Benchmark | YCbCrRatio420 | 75 | 6.463 ms | 0.0282 ms | 0.0235 ms | +| Benchmark | YCbCrRatio444 | 75 | 8.616 ms | 0.0422 ms | 0.0374 ms | +| Benchmark | Luminance | 90 | 7.011 ms | 0.0361 ms | 0.0301 ms | +| Benchmark | Rgb | 90 | 13.119 ms | 0.0947 ms | 0.0886 ms | +| Benchmark | YCbCrRatio420 | 90 | 6.786 ms | 0.0328 ms | 0.0274 ms | +| Benchmark | YCbCrRatio444 | 90 | 8.672 ms | 0.0772 ms | 0.0722 ms | +| Benchmark | Luminance | 100 | 9.554 ms | 0.1211 ms | 0.1012 ms | +| Benchmark | Rgb | 100 | 19.475 ms | 0.1080 ms | 0.0958 ms | +| Benchmark | YCbCrRatio420 | 100 | 10.146 ms | 0.0585 ms | 0.0519 ms | +| Benchmark | YCbCrRatio444 | 100 | 15.317 ms | 0.0709 ms | 0.0592 ms | +*/ diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/EncodeJpegMultiple.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/EncodeJpegMultiple.cs deleted file mode 100644 index 71276597a..000000000 --- a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/EncodeJpegMultiple.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -using System.Collections.Generic; -using System.Drawing.Imaging; -using BenchmarkDotNet.Attributes; -using SixLabors.ImageSharp.Formats.Jpeg; - -namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg -{ - [Config(typeof(Config.ShortMultiFramework))] - public class EncodeJpegMultiple : MultiImageBenchmarkBase.WithImagesPreloaded - { - protected override IEnumerable InputImageSubfoldersOrFiles => new[] { "Bmp/", "Jpg/baseline" }; - - protected override IEnumerable SearchPatterns => new[] { "*.bmp", "*.jpg" }; - - [Benchmark(Description = "EncodeJpegMultiple - ImageSharp")] - public void EncodeJpegImageSharp() - => this.ForEachImageSharpImage((img, ms) => - { - img.Save(ms, new JpegEncoder()); - return null; - }); - - [Benchmark(Baseline = true, Description = "EncodeJpegMultiple - System.Drawing")] - public void EncodeJpegSystemDrawing() - => this.ForEachSystemDrawingImage((img, ms) => - { - img.Save(ms, ImageFormat.Jpeg); - return null; - }); - } -} diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/IdentifyJpeg.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/IdentifyJpeg.cs index aae144ce0..7bb147cf9 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/IdentifyJpeg.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/IdentifyJpeg.cs @@ -32,7 +32,7 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs.Jpeg { using var memoryStream = new MemoryStream(this.jpegBytes); var decoder = new JpegDecoder(); - return decoder.Identify(Configuration.Default, memoryStream); + return decoder.Identify(Configuration.Default, memoryStream, default); } } } diff --git a/tests/ImageSharp.Benchmarks/Codecs/Png/DecodeFilteredPng.cs b/tests/ImageSharp.Benchmarks/Codecs/Png/DecodeFilteredPng.cs index 6517bf3c4..5f91a050e 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Png/DecodeFilteredPng.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Png/DecodeFilteredPng.cs @@ -16,16 +16,18 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs private byte[] filter1; private byte[] filter2; private byte[] filter3; - private byte[] filter4; + private byte[] averageFilter3bpp; + private byte[] averageFilter4bpp; [GlobalSetup] public void ReadImages() { this.filter0 = File.ReadAllBytes(TestImageFullPath(TestImages.Png.Filter0)); - this.filter1 = File.ReadAllBytes(TestImageFullPath(TestImages.Png.Filter1)); - this.filter2 = File.ReadAllBytes(TestImageFullPath(TestImages.Png.Filter2)); - this.filter3 = File.ReadAllBytes(TestImageFullPath(TestImages.Png.Filter3)); - this.filter4 = File.ReadAllBytes(TestImageFullPath(TestImages.Png.Filter4)); + this.filter1 = File.ReadAllBytes(TestImageFullPath(TestImages.Png.SubFilter3BytesPerPixel)); + this.filter2 = File.ReadAllBytes(TestImageFullPath(TestImages.Png.UpFilter)); + this.filter3 = File.ReadAllBytes(TestImageFullPath(TestImages.Png.PaethFilter3BytesPerPixel)); + this.averageFilter3bpp = File.ReadAllBytes(TestImageFullPath(TestImages.Png.AverageFilter3BytesPerPixel)); + this.averageFilter4bpp = File.ReadAllBytes(TestImageFullPath(TestImages.Png.AverageFilter4BytesPerPixel)); } [Benchmark(Baseline = true, Description = "None-filtered PNG file")] @@ -40,13 +42,17 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs public Size PngFilter2() => LoadPng(this.filter2); - [Benchmark(Description = "Average-filtered PNG file")] - public Size PngFilter3() - => LoadPng(this.filter3); + [Benchmark(Description = "Average-filtered PNG file (3bpp)")] + public Size PngAvgFilter1() + => LoadPng(this.averageFilter3bpp); + + [Benchmark(Description = "Average-filtered PNG file (4bpp)")] + public Size PngAvgFilter2() + => LoadPng(this.averageFilter4bpp); [Benchmark(Description = "Paeth-filtered PNG file")] public Size PngFilter4() - => LoadPng(this.filter4); + => LoadPng(this.filter3); [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Size LoadPng(byte[] bytes) diff --git a/tests/ImageSharp.Benchmarks/Config.cs b/tests/ImageSharp.Benchmarks/Config.cs index 299784821..60d0e7661 100644 --- a/tests/ImageSharp.Benchmarks/Config.cs +++ b/tests/ImageSharp.Benchmarks/Config.cs @@ -34,7 +34,7 @@ namespace SixLabors.ImageSharp.Benchmarks public MultiFramework() => this.AddJob( Job.Default.WithRuntime(ClrRuntime.Net472), Job.Default.WithRuntime(CoreRuntime.Core31), - Job.Default.WithRuntime(CoreRuntime.Core50).With(new Argument[] { new MsBuildArgument("/p:DebugType=portable") })); + Job.Default.WithRuntime(CoreRuntime.Core50).WithArguments(new Argument[] { new MsBuildArgument("/p:DebugType=portable") })); } public class ShortMultiFramework : Config @@ -42,7 +42,7 @@ namespace SixLabors.ImageSharp.Benchmarks public ShortMultiFramework() => this.AddJob( Job.Default.WithRuntime(ClrRuntime.Net472).WithLaunchCount(1).WithWarmupCount(3).WithIterationCount(3), Job.Default.WithRuntime(CoreRuntime.Core31).WithLaunchCount(1).WithWarmupCount(3).WithIterationCount(3), - Job.Default.WithRuntime(CoreRuntime.Core50).WithLaunchCount(1).WithWarmupCount(3).WithIterationCount(3).With(new Argument[] { new MsBuildArgument("/p:DebugType=portable") })); + Job.Default.WithRuntime(CoreRuntime.Core50).WithLaunchCount(1).WithWarmupCount(3).WithIterationCount(3).WithArguments(new Argument[] { new MsBuildArgument("/p:DebugType=portable") })); } public class ShortCore31 : Config diff --git a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj index 9a9274199..24f618d11 100644 --- a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj +++ b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj @@ -9,7 +9,7 @@ portable false - Debug;Release;Debug-InnerLoop;Release-InnerLoop + Debug;Release @@ -17,17 +17,12 @@ - net6.0;net5.0;netcoreapp3.1;netcoreapp2.1;net472 - - - - - netcoreapp3.1 + net7.0;net6.0 - net5.0;netcoreapp3.1;netcoreapp2.1;net472 + net6.0 diff --git a/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs b/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs index eda054968..58da1f8d5 100644 --- a/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs +++ b/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; @@ -13,7 +14,6 @@ using System.Threading.Tasks; using ImageMagick; using PhotoSauce.MagicScaler; using SixLabors.ImageSharp.Formats.Jpeg; -using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Tests; using SkiaSharp; @@ -124,6 +124,32 @@ namespace SixLabors.ImageSharp.Benchmarks.LoadResizeSave new ParallelOptions { MaxDegreeOfParallelism = this.MaxDegreeOfParallelism }, action); + public Task ForEachImageParallelAsync(Func action) + { + int maxDegreeOfParallelism = this.MaxDegreeOfParallelism > 0 + ? this.MaxDegreeOfParallelism + : Environment.ProcessorCount; + int partitionSize = (int)Math.Ceiling((double)this.Images.Length / maxDegreeOfParallelism); + + List tasks = new(); + for (int i = 0; i < this.Images.Length; i += partitionSize) + { + int end = Math.Min(i + partitionSize, this.Images.Length); + Task task = RunPartition(i, end); + tasks.Add(task); + } + + return Task.WhenAll(tasks); + + Task RunPartition(int start, int end) => Task.Run(async () => + { + for (int i = start; i < end; i++) + { + await action(this.Images[i]); + } + }); + } + private void LogImageProcessed(int width, int height) { this.LastProcessedImageSize = new Size(width, height); @@ -197,6 +223,27 @@ namespace SixLabors.ImageSharp.Benchmarks.LoadResizeSave image.Save(output, this.imageSharpJpegEncoder); } + public async Task ImageSharpResizeAsync(string input) + { + using FileStream output = File.Open(this.OutputPath(input), FileMode.Create); + + // Resize it to fit a 150x150 square. + using ImageSharpImage image = await ImageSharpImage.LoadAsync(input); + this.LogImageProcessed(image.Width, image.Height); + + image.Mutate(i => i.Resize(new ResizeOptions + { + Size = new ImageSharpSize(this.ThumbnailSize, this.ThumbnailSize), + Mode = ResizeMode.Max + })); + + // Reduce the size of the file + image.Metadata.ExifProfile = null; + + // Save the results + await image.SaveAsync(output, this.imageSharpJpegEncoder); + } + public void MagickResize(string input) { using var image = new MagickImage(input); diff --git a/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj b/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj index 6ff5a4cc7..492ce36b8 100644 --- a/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj +++ b/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj @@ -12,24 +12,19 @@ false false - Debug;Release;Debug-InnerLoop;Release-InnerLoop + Debug;Release false - net6.0;net5.0;netcoreapp3.1;netcoreapp2.1;net472 - - - - - netcoreapp3.1 + net7.0;net6.0 - net5.0;netcoreapp3.1;netcoreapp2.1;net472 + net6.0 diff --git a/tests/ImageSharp.Tests.ProfilingSandbox/LoadResizeSaveParallelMemoryStress.cs b/tests/ImageSharp.Tests.ProfilingSandbox/LoadResizeSaveParallelMemoryStress.cs index c7484daa0..c5f312460 100644 --- a/tests/ImageSharp.Tests.ProfilingSandbox/LoadResizeSaveParallelMemoryStress.cs +++ b/tests/ImageSharp.Tests.ProfilingSandbox/LoadResizeSaveParallelMemoryStress.cs @@ -5,7 +5,6 @@ using System; using System.Diagnostics; using System.Globalization; using System.IO; -using System.Runtime.CompilerServices; using System.Text; using System.Threading; using CommandLine; @@ -51,7 +50,7 @@ namespace SixLabors.ImageSharp.Tests.ProfilingSandbox Console.WriteLine($"\nEnvironment.ProcessorCount={Environment.ProcessorCount}"); Stopwatch timer; - if (options == null || !options.ImageSharp) + if (options == null || !(options.ImageSharp || options.AsyncImageSharp)) { RunBenchmarkSwitcher(lrs, out timer); } @@ -68,13 +67,21 @@ namespace SixLabors.ImageSharp.Tests.ProfilingSandbox lrs.leakFrequency = options.LeakFrequency; lrs.gcFrequency = options.GcFrequency; - timer = Stopwatch.StartNew(); try { for (int i = 0; i < options.RepeatCount; i++) { - lrs.ImageSharpBenchmarkParallel(); + if (options.AsyncImageSharp) + { + lrs.ImageSharpBenchmarkParallelAsync(); + } + else + { + lrs.ImageSharpBenchmarkParallel(); + } + + Console.WriteLine("OK"); } } catch (Exception ex) @@ -221,6 +228,9 @@ namespace SixLabors.ImageSharp.Tests.ProfilingSandbox private class CommandLineOptions { + [Option('a', "async-imagesharp", Required = false, Default = false, HelpText = "Async ImageSharp without benchmark switching")] + public bool AsyncImageSharp { get; set; } + [Option('i', "imagesharp", Required = false, Default = false, HelpText = "Test ImageSharp without benchmark switching")] public bool ImageSharp { get; set; } @@ -277,7 +287,7 @@ namespace SixLabors.ImageSharp.Tests.ProfilingSandbox } public override string ToString() => - $"p({this.MaxDegreeOfParallelism})_i({this.ImageSharp})_d({this.KeepDefaultAllocator})_m({this.MaxContiguousPoolBufferMegaBytes})_s({this.MaxPoolSizeMegaBytes})_u({this.MaxCapacityOfNonPoolBuffersMegaBytes})_r({this.RepeatCount})_g({this.GcFrequency})_e({this.ReleaseRetainedResourcesAtEnd})_l({this.LeakFrequency})"; + $"p({this.MaxDegreeOfParallelism})_i({this.ImageSharp})_a({this.AsyncImageSharp})_d({this.KeepDefaultAllocator})_m({this.MaxContiguousPoolBufferMegaBytes})_s({this.MaxPoolSizeMegaBytes})_u({this.MaxCapacityOfNonPoolBuffersMegaBytes})_r({this.RepeatCount})_g({this.GcFrequency})_e({this.ReleaseRetainedResourcesAtEnd})_l({this.LeakFrequency})"; public MemoryAllocator CreateMemoryAllocator() { @@ -330,11 +340,17 @@ namespace SixLabors.ImageSharp.Tests.ProfilingSandbox } }); + private void ImageSharpBenchmarkParallelAsync() => + this.Benchmarks.ForEachImageParallelAsync(f => this.Benchmarks.ImageSharpResizeAsync(f)) + .GetAwaiter() + .GetResult(); + private void MagickBenchmarkParallel() => this.ForEachImage(this.Benchmarks.MagickResize); private void MagicScalerBenchmarkParallel() => this.ForEachImage(this.Benchmarks.MagicScalerResize); private void SkiaBitmapBenchmarkParallel() => this.ForEachImage(this.Benchmarks.SkiaBitmapResize); + private void SkiaBitmapDecodeToTargetSizeBenchmarkParallel() => this.ForEachImage(this.Benchmarks.SkiaBitmapDecodeToTargetSize); private void NetVipsBenchmarkParallel() => this.ForEachImage(this.Benchmarks.NetVipsResize); diff --git a/tests/ImageSharp.Tests.ruleset b/tests/ImageSharp.Tests.ruleset index 673f00517..50c275cd7 100644 --- a/tests/ImageSharp.Tests.ruleset +++ b/tests/ImageSharp.Tests.ruleset @@ -1,6 +1,9 @@  - + + + + diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs index f85bc78fc..b42569a23 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs @@ -20,6 +20,7 @@ using static SixLabors.ImageSharp.Tests.TestImages.Bmp; namespace SixLabors.ImageSharp.Tests.Formats.Bmp { [Trait("Format", "Bmp")] + [ValidateDisposedMemoryAllocations] public class BmpDecoderTests { public const PixelTypes CommonNonDefaultPixelTypes = PixelTypes.Rgba32 | PixelTypes.Bgra32 | PixelTypes.RgbaVector; @@ -28,10 +29,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp public static readonly string[] BitfieldsBmpFiles = BitFields; - private static BmpDecoder BmpDecoder => new BmpDecoder(); + private static BmpDecoder BmpDecoder => new(); public static readonly TheoryData RatioFiles = - new TheoryData + new() { { Car, 3780, 3780, PixelResolutionUnit.PixelsPerMeter }, { V5Header, 3780, 3780, PixelResolutionUnit.PixelsPerMeter }, @@ -557,7 +558,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp using (var stream = new MemoryStream(testFile.Bytes, false)) { var decoder = new BmpDecoder(); - using (Image image = decoder.Decode(Configuration.Default, stream)) + using (Image image = decoder.Decode(Configuration.Default, stream, default)) { ImageMetadata meta = image.Metadata; Assert.Equal(xResolution, meta.HorizontalResolution); diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs index 073cf5fcf..2215d5f0a 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs @@ -301,6 +301,33 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp public void Encode_PreservesAlpha(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) where TPixel : unmanaged, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true); + [Theory] + [WithFile(IccProfile, PixelTypes.Rgba32)] + public void Encode_PreservesColorProfile(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image input = provider.GetImage(new BmpDecoder())) + { + ImageSharp.Metadata.Profiles.Icc.IccProfile expectedProfile = input.Metadata.IccProfile; + byte[] expectedProfileBytes = expectedProfile.ToByteArray(); + + using (var memStream = new MemoryStream()) + { + input.Save(memStream, new BmpEncoder()); + + memStream.Position = 0; + using (var output = Image.Load(memStream)) + { + ImageSharp.Metadata.Profiles.Icc.IccProfile actualProfile = output.Metadata.IccProfile; + byte[] actualProfileBytes = actualProfile.ToByteArray(); + + Assert.NotNull(actualProfile); + Assert.Equal(expectedProfileBytes, actualProfileBytes); + } + } + } + } + [Theory] [WithFile(Car, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel32)] [WithFile(V5Header, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel32)] diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpMetadataTests.cs index 8931c242e..6a582e019 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpMetadataTests.cs @@ -3,7 +3,7 @@ using System.IO; using SixLabors.ImageSharp.Formats.Bmp; - +using SixLabors.ImageSharp.PixelFormats; using Xunit; using static SixLabors.ImageSharp.Tests.TestImages.Bmp; @@ -47,5 +47,19 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp Assert.Equal(expectedInfoHeaderType, bitmapMetadata.InfoHeaderType); } } + + [Theory] + [WithFile(IccProfile, PixelTypes.Rgba32)] + public void Decoder_CanReadColorProfile(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = provider.GetImage(new BmpDecoder())) + { + ImageSharp.Metadata.ImageMetadata metaData = image.Metadata; + Assert.NotNull(metaData); + Assert.NotNull(metaData.IccProfile); + Assert.Equal(16, metaData.IccProfile.Entries.Length); + } + } } } diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs index 6bf606ac9..7a5241c5a 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs @@ -18,6 +18,7 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Formats.Gif { [Trait("Format", "Gif")] + [ValidateDisposedMemoryAllocations] public class GifDecoderTests { private const PixelTypes TestPixelTypes = PixelTypes.Rgba32 | PixelTypes.RgbaVector | PixelTypes.Argb32; @@ -52,7 +53,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif { using (var stream = new UnmanagedMemoryStream(data, length)) { - using (Image image = GifDecoder.Decode(Configuration.Default, stream)) + using (Image image = GifDecoder.Decode(Configuration.Default, stream, default)) { Assert.Equal((200, 200), (image.Width, image.Height)); } @@ -247,5 +248,58 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif "Disco") .Dispose(); } + + // https://github.com/SixLabors/ImageSharp/issues/1962 + [Theory] + [WithFile(TestImages.Gif.Issues.Issue1962NoColorTable, PixelTypes.Rgba32)] + public void Issue1962(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + image.DebugSave(provider); + + image.CompareFirstFrameToReferenceOutput(ImageComparer.Exact, provider); + } + + // https://github.com/SixLabors/ImageSharp/issues/2012 + [Theory] + [WithFile(TestImages.Gif.Issues.Issue2012EmptyXmp, PixelTypes.Rgba32)] + public void Issue2012EmptyXmp(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + + image.DebugSave(provider); + image.CompareFirstFrameToReferenceOutput(ImageComparer.Exact, provider); + } + + // https://github.com/SixLabors/ImageSharp/issues/2012 + [Theory] + [WithFile(TestImages.Gif.Issues.Issue2012BadMinCode, PixelTypes.Rgba32)] + public void Issue2012BadMinCode(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Exception ex = Record.Exception( + () => + { + using Image image = provider.GetImage(); + image.DebugSave(provider); + }); + + Assert.NotNull(ex); + Assert.Contains("Gif Image does not contain a valid LZW minimum code.", ex.Message); + } + + // https://bugzilla.mozilla.org/show_bug.cgi?id=55918 + [Theory] + [WithFile(TestImages.Gif.Issues.DeferredClearCode, PixelTypes.Rgba32)] + public void IssueDeferredClearCode(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + + image.DebugSave(provider); + image.CompareFirstFrameToReferenceOutput(ImageComparer.Exact, provider); + } } } diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs index 5699b4741..efabed5b2 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs @@ -117,7 +117,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif input.Save(memoryStream, new GifEncoder()); memoryStream.Position = 0; - using (Image image = decoder.Decode(Configuration.Default, memoryStream)) + using (Image image = decoder.Decode(Configuration.Default, memoryStream, default)) { GifMetadata metadata = image.Metadata.GetGifMetadata(); Assert.Equal(2, metadata.Comments.Count); @@ -135,7 +135,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif using (var stream = new MemoryStream(testFile.Bytes, false)) { var decoder = new GifDecoder(); - IImageInfo image = decoder.Identify(Configuration.Default, stream); + IImageInfo image = decoder.Identify(Configuration.Default, stream, default); ImageMetadata meta = image.Metadata; Assert.Equal(xResolution, meta.HorizontalResolution); Assert.Equal(yResolution, meta.VerticalResolution); @@ -151,7 +151,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif using (var stream = new MemoryStream(testFile.Bytes, false)) { var decoder = new GifDecoder(); - using (Image image = decoder.Decode(Configuration.Default, stream)) + using (Image image = decoder.Decode(Configuration.Default, stream, default)) { ImageMetadata meta = image.Metadata; Assert.Equal(xResolution, meta.HorizontalResolution); @@ -169,7 +169,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif using (var stream = new MemoryStream(testFile.Bytes, false)) { var decoder = new GifDecoder(); - IImageInfo image = decoder.Identify(Configuration.Default, stream); + IImageInfo image = decoder.Identify(Configuration.Default, stream, default); GifMetadata meta = image.Metadata.GetGifMetadata(); Assert.Equal(repeatCount, meta.RepeatCount); } @@ -183,7 +183,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif using (var stream = new MemoryStream(testFile.Bytes, false)) { var decoder = new GifDecoder(); - using (Image image = decoder.Decode(Configuration.Default, stream)) + using (Image image = decoder.Decode(Configuration.Default, stream, default)) { GifMetadata meta = image.Metadata.GetGifMetadata(); Assert.Equal(repeatCount, meta.RepeatCount); diff --git a/tests/ImageSharp.Tests/Formats/Jpg/DCTTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/DCTTests.cs index 9c467a1cc..a40ae2af5 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/DCTTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/DCTTests.cs @@ -149,11 +149,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg // 1. AllowAll - call avx/fma implementation // 2. DisableFMA - call avx without fma implementation // 3. DisableAvx - call sse implementation - // 4. DisableSIMD - call Vector4 fallback implementation + // 4. DisableHWIntrinsic - call Vector4 fallback implementation FeatureTestRunner.RunWithHwIntrinsicsFeature( RunTest, seed, - HwIntrinsics.AllowAll | HwIntrinsics.DisableFMA | HwIntrinsics.DisableAVX | HwIntrinsics.DisableSIMD); + HwIntrinsics.AllowAll | HwIntrinsics.DisableFMA | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); } // Forward transform @@ -200,11 +200,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg // 1. AllowAll - call avx/fma implementation // 2. DisableFMA - call avx without fma implementation // 3. DisableAvx - call Vector4 implementation - // 4. DisableSIMD - call scalar fallback implementation + // 4. DisableHWIntrinsic - call scalar fallback implementation FeatureTestRunner.RunWithHwIntrinsicsFeature( RunTest, seed, - HwIntrinsics.AllowAll | HwIntrinsics.DisableFMA | HwIntrinsics.DisableAVX | HwIntrinsics.DisableSIMD); + HwIntrinsics.AllowAll | HwIntrinsics.DisableFMA | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); } } } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Baseline.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Baseline.cs index 021e3d272..9aaf2b968 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Baseline.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Baseline.cs @@ -3,7 +3,7 @@ using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.TestUtilities; - +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using Xunit; // ReSharper disable InconsistentNaming @@ -49,6 +49,20 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg // .Dispose(); } + [Theory] + [WithFile(TestImages.Jpeg.Baseline.ArithmeticCoding01, PixelTypes.Rgb24)] + [WithFile(TestImages.Jpeg.Baseline.ArithmeticCoding02, PixelTypes.Rgb24)] + [WithFile(TestImages.Jpeg.Baseline.ArithmeticCodingGray, PixelTypes.Rgb24)] + [WithFile(TestImages.Jpeg.Baseline.ArithmeticCodingInterleaved, PixelTypes.Rgb24)] + [WithFile(TestImages.Jpeg.Baseline.ArithmeticCodingWithRestart, PixelTypes.Rgb24)] + public void DecodeJpeg_WithArithmeticCoding(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(JpegDecoder); + image.DebugSave(provider); + image.CompareToOriginal(provider, ImageComparer.Tolerant(0.002f), ReferenceDecoder); + } + [Theory] [WithFileCollection(nameof(UnrecoverableTestJpegs), PixelTypes.Rgba32)] public void UnrecoverableImage_Throws_InvalidImageContentException(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs index db5169a04..543619c87 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs @@ -42,6 +42,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg // High depth images TestImages.Jpeg.Baseline.Testorig12bit, + + // Grayscale jpeg with 2x2 sampling factors (not a usual thing to encounter in the wild) + TestImages.Jpeg.Baseline.GrayscaleSampling2x2, }; public static string[] ProgressiveTestJpegs = @@ -69,10 +72,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg TestImages.Jpeg.Issues.Fuzz.NullReferenceException823, TestImages.Jpeg.Issues.MalformedUnsupportedComponentCount, - // Arithmetic coding - TestImages.Jpeg.Baseline.ArithmeticCoding, - TestImages.Jpeg.Baseline.ArithmeticCodingProgressive, - // Lossless jpeg TestImages.Jpeg.Baseline.Lossless }; @@ -102,6 +101,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg TestImages.Jpeg.Issues.Fuzz.IndexOutOfRangeException1693A, TestImages.Jpeg.Issues.Fuzz.IndexOutOfRangeException1693B, TestImages.Jpeg.Issues.Fuzz.IndexOutOfRangeException824C, + TestImages.Jpeg.Issues.Fuzz.NullReferenceException2085, }; private static readonly Dictionary CustomToleranceValues = new() diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs index 53c81631d..d9915f17d 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs @@ -83,7 +83,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg using (var stream = new MemoryStream(testFile.Bytes, false)) { var decoder = new JpegDecoder(); - using (Image image = decoder.Decode(Configuration.Default, stream)) + using (Image image = decoder.Decode(Configuration.Default, stream, default)) { ImageMetadata meta = image.Metadata; Assert.Equal(xResolution, meta.HorizontalResolution); @@ -101,7 +101,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg using (var stream = new MemoryStream(testFile.Bytes, false)) { var decoder = new JpegDecoder(); - IImageInfo image = decoder.Identify(Configuration.Default, stream); + IImageInfo image = decoder.Identify(Configuration.Default, stream, default); ImageMetadata meta = image.Metadata; Assert.Equal(xResolution, meta.HorizontalResolution); Assert.Equal(yResolution, meta.VerticalResolution); @@ -117,7 +117,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg using (var stream = new MemoryStream(testFile.Bytes, false)) { var decoder = new JpegDecoder(); - IImageInfo image = decoder.Identify(Configuration.Default, stream); + IImageInfo image = decoder.Identify(Configuration.Default, stream, default); JpegMetadata meta = image.Metadata.GetJpegMetadata(); Assert.Equal(quality, meta.Quality); } @@ -130,7 +130,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg var testFile = TestFile.Create(imagePath); using (var stream = new MemoryStream(testFile.Bytes, false)) { - using (Image image = JpegDecoder.Decode(Configuration.Default, stream)) + using (Image image = JpegDecoder.Decode(Configuration.Default, stream, default)) { JpegMetadata meta = image.Metadata.GetJpegMetadata(); Assert.Equal(quality, meta.Quality); @@ -152,7 +152,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg var testFile = TestFile.Create(imagePath); using (var stream = new MemoryStream(testFile.Bytes, false)) { - IImageInfo image = JpegDecoder.Identify(Configuration.Default, stream); + IImageInfo image = JpegDecoder.Identify(Configuration.Default, stream, default); JpegMetadata meta = image.Metadata.GetJpegMetadata(); Assert.Equal(expectedColorType, meta.ColorType); } @@ -179,11 +179,16 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg var testFile = TestFile.Create(imagePath); using (var stream = new MemoryStream(testFile.Bytes, false)) { - IImageInfo imageInfo = useIdentify - ? ((IImageInfoDetector)decoder).Identify(Configuration.Default, stream) - : decoder.Decode(Configuration.Default, stream); - - test(imageInfo); + if (useIdentify) + { + IImageInfo imageInfo = ((IImageInfoDetector)decoder).Identify(Configuration.Default, stream, default); + test(imageInfo); + } + else + { + using var img = decoder.Decode(Configuration.Default, stream, default); + test(img); + } } } @@ -300,5 +305,85 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg }); Assert.Null(ex); } + + [Theory] + [WithFile(TestImages.Jpeg.Issues.ExifNullArrayTag, PixelTypes.Rgba32)] + public void Clone_WithNullRationalArrayTag_DoesNotThrowException(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Exception ex = Record.Exception(() => + { + using Image image = provider.GetImage(JpegDecoder); + var clone = image.Metadata.ExifProfile.DeepClone(); + }); + Assert.Null(ex); + } + + [Fact] + public void EncodedStringTags_WriteAndRead() + { + using var memoryStream = new MemoryStream(); + using (var image = Image.Load(TestFile.GetInputFileFullPath(TestImages.Jpeg.Baseline.Calliphora))) + { + var exif = new ExifProfile(); + + exif.SetValue(ExifTag.GPSDateStamp, "2022-01-06"); + + exif.SetValue(ExifTag.XPTitle, "A bit of test metadata for image title"); + exif.SetValue(ExifTag.XPComment, "A bit of test metadata for image comment"); + exif.SetValue(ExifTag.XPAuthor, "Dan Petitt"); + exif.SetValue(ExifTag.XPKeywords, "Keyword1;Keyword2"); + exif.SetValue(ExifTag.XPSubject, "This is a subject"); + + // exif.SetValue(ExifTag.UserComment, new EncodedString(EncodedString.CharacterCode.JIS, "ビッ")); + exif.SetValue(ExifTag.UserComment, new EncodedString(EncodedString.CharacterCode.JIS, "eng comment text (JIS)")); + + exif.SetValue(ExifTag.GPSProcessingMethod, new EncodedString(EncodedString.CharacterCode.ASCII, "GPS processing method (ASCII)")); + exif.SetValue(ExifTag.GPSAreaInformation, new EncodedString(EncodedString.CharacterCode.Unicode, "GPS area info (Unicode)")); + + image.Metadata.ExifProfile = exif; + + image.Save(memoryStream, new JpegEncoder()); + } + + memoryStream.Seek(0, SeekOrigin.Begin); + using (var image = Image.Load(memoryStream)) + { + ExifProfile exif = image.Metadata.ExifProfile; + VerifyEncodedStrings(exif); + } + } + + [Fact] + public void EncodedStringTags_Read() + { + using (var image = Image.Load(TestFile.GetInputFileFullPath(TestImages.Jpeg.Baseline.Calliphora_EncodedStrings))) + { + ExifProfile exif = image.Metadata.ExifProfile; + VerifyEncodedStrings(exif); + } + } + + private static void VerifyEncodedStrings(ExifProfile exif) + { + Assert.NotNull(exif); + + Assert.Equal("2022-01-06", exif.GetValue(ExifTag.GPSDateStamp).Value); + + Assert.Equal("A bit of test metadata for image title", exif.GetValue(ExifTag.XPTitle).Value); + Assert.Equal("A bit of test metadata for image comment", exif.GetValue(ExifTag.XPComment).Value); + Assert.Equal("Dan Petitt", exif.GetValue(ExifTag.XPAuthor).Value); + Assert.Equal("Keyword1;Keyword2", exif.GetValue(ExifTag.XPKeywords).Value); + Assert.Equal("This is a subject", exif.GetValue(ExifTag.XPSubject).Value); + + Assert.Equal("eng comment text (JIS)", exif.GetValue(ExifTag.UserComment).Value.Text); + Assert.Equal(EncodedString.CharacterCode.JIS, exif.GetValue(ExifTag.UserComment).Value.Code); + + Assert.Equal("GPS processing method (ASCII)", exif.GetValue(ExifTag.GPSProcessingMethod).Value.Text); + Assert.Equal(EncodedString.CharacterCode.ASCII, exif.GetValue(ExifTag.GPSProcessingMethod).Value.Code); + + Assert.Equal("GPS area info (Unicode)", (string)exif.GetValue(ExifTag.GPSAreaInformation).Value); + Assert.Equal(EncodedString.CharacterCode.Unicode, exif.GetValue(ExifTag.GPSAreaInformation).Value.Code); + } } } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Progressive.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Progressive.cs index e8533b9bc..30e5da4ef 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Progressive.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Progressive.cs @@ -4,6 +4,7 @@ using Microsoft.DotNet.RemoteExecutor; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.TestUtilities; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using Xunit; // ReSharper disable InconsistentNaming @@ -29,6 +30,17 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg appendPixelTypeToFileName: false); } + [Theory] + [WithFile(TestImages.Jpeg.Baseline.ArithmeticCodingProgressive01, PixelTypes.Rgb24)] + [WithFile(TestImages.Jpeg.Baseline.ArithmeticCodingProgressive02, PixelTypes.Rgb24)] + public void DecodeProgressiveJpeg_WithArithmeticCoding(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(JpegDecoder); + image.DebugSave(provider); + image.CompareToOriginal(provider, ImageComparer.Tolerant(0.004f), ReferenceDecoder); + } + [Theory] [WithFile(TestImages.Jpeg.Progressive.Progress, PixelTypes.Rgb24)] public void DecodeProgressiveJpeg_WithLimitedAllocatorBufferCapacity(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs index 7a2446959..e39aaa323 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs @@ -13,7 +13,7 @@ using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.Formats.Jpg.Utils; using SixLabors.ImageSharp.Tests.TestUtilities; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; - +using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; using Xunit; using Xunit.Abstractions; @@ -21,9 +21,12 @@ using Xunit.Abstractions; namespace SixLabors.ImageSharp.Tests.Formats.Jpg { // TODO: Scatter test cases into multiple test classes - [Trait("Format", "Jpg")] + [Trait("Format", "Jpg")] + [ValidateDisposedMemoryAllocations] public partial class JpegDecoderTests { + private static MagickReferenceDecoder ReferenceDecoder => new(); + public const PixelTypes CommonNonDefaultPixelTypes = PixelTypes.Rgba32 | PixelTypes.Argb32 | PixelTypes.Bgr24 | PixelTypes.RgbaVector; private const float BaselineTolerance = 0.001F / 100; @@ -65,7 +68,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg private ITestOutputHelper Output { get; } - private static JpegDecoder JpegDecoder => new JpegDecoder(); + private static JpegDecoder JpegDecoder => new(); [Fact] public void ParseStream_BasicPropertiesAreCorrect() @@ -139,27 +142,16 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg Assert.IsType(ex.InnerException); } - [Theory] - [InlineData(0)] - [InlineData(0.5)] - [InlineData(0.9)] - public async Task DecodeAsync_IsCancellable(int percentageOfStreamReadToCancel) + [Fact] + public async Task DecodeAsync_IsCancellable() { var cts = new CancellationTokenSource(); string file = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImages.Jpeg.Baseline.Jpeg420Small); using var pausedStream = new PausedStream(file); pausedStream.OnWaiting(s => { - if (s.Position >= s.Length * percentageOfStreamReadToCancel) - { - cts.Cancel(); - pausedStream.Release(); - } - else - { - // allows this/next wait to unblock - pausedStream.Next(); - } + cts.Cancel(); + pausedStream.Release(); }); var config = Configuration.CreateDefaultInstance(); @@ -213,6 +205,19 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg } } + // https://github.com/SixLabors/ImageSharp/issues/2057 + [Theory] + [WithFile(TestImages.Jpeg.Issues.Issue2057App1Parsing, PixelTypes.Rgba32)] + public void Issue2057_DecodeWorks(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = provider.GetImage(JpegDecoder)) + { + image.DebugSave(provider); + image.CompareToOriginal(provider); + } + } + // DEBUG ONLY! // The PDF.js output should be saved by "tests\ImageSharp.Tests\Formats\Jpg\pdfjs\jpeg-converter.htm" // into "\tests\Images\ActualOutput\JpegDecoderTests\" diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.Metadata.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.Metadata.cs new file mode 100644 index 000000000..2bbce6cb1 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.Metadata.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.PixelFormats; + +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Jpg +{ + [Trait("Format", "Jpg")] + public partial class JpegEncoderTests + { + [Theory] + [WithFile(TestImages.Jpeg.Issues.ValidExifArgumentNullExceptionOnEncode, PixelTypes.Rgba32)] + public void Encode_WithValidExifProfile_DoesNotThrowException(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Exception ex = Record.Exception(() => + { + var encoder = new JpegEncoder(); + var stream = new MemoryStream(); + + using Image image = provider.GetImage(JpegDecoder); + image.Save(stream, encoder); + }); + + Assert.Null(ex); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs index 18eae9fbd..d860836e0 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs @@ -20,7 +20,7 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Formats.Jpg { [Trait("Format", "Jpg")] - public class JpegEncoderTests + public partial class JpegEncoderTests { private static JpegEncoder JpegEncoder => new(); diff --git a/tests/ImageSharp.Tests/Formats/Jpg/ParseStreamTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/ParseStreamTests.cs index c4d0faf33..714d99300 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/ParseStreamTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/ParseStreamTests.cs @@ -50,7 +50,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg Assert.Equal(expectedSizeInBlocks, decoder.Frame.McuSize); var uniform1 = new Size(1, 1); - JpegComponent c0 = decoder.Components[0]; + IJpegComponent c0 = decoder.Components[0]; VerifyJpeg.VerifyComponent(c0, expectedSizeInBlocks, uniform1, uniform1); } } @@ -70,8 +70,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg { sb.AppendLine(imageFile); sb.AppendLine($"Size:{decoder.Frame.PixelSize} MCU:{decoder.Frame.McuSize}"); - JpegComponent c0 = decoder.Components[0]; - JpegComponent c1 = decoder.Components[1]; + IJpegComponent c0 = decoder.Components[0]; + IJpegComponent c1 = decoder.Components[1]; sb.AppendLine($"Luma: SAMP: {c0.SamplingFactors} BLOCKS: {c0.SizeInBlocks}"); sb.AppendLine($"Chroma: {c1.SamplingFactors} BLOCKS: {c1.SizeInBlocks}"); @@ -80,17 +80,17 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg this.Output.WriteLine(sb.ToString()); } - public static readonly TheoryData ComponentVerificationData = new TheoryData - { - { TestImages.Jpeg.Baseline.Jpeg444, 3, new Size(1, 1), new Size(1, 1) }, - { TestImages.Jpeg.Baseline.Jpeg420Exif, 3, new Size(2, 2), new Size(1, 1) }, - { TestImages.Jpeg.Baseline.Jpeg420Small, 3, new Size(2, 2), new Size(1, 1) }, - { TestImages.Jpeg.Baseline.Testorig420, 3, new Size(2, 2), new Size(1, 1) }, + public static readonly TheoryData ComponentVerificationData = new() + { + { TestImages.Jpeg.Baseline.Jpeg444, 3, new Size(1, 1), new Size(1, 1) }, + { TestImages.Jpeg.Baseline.Jpeg420Exif, 3, new Size(2, 2), new Size(1, 1) }, + { TestImages.Jpeg.Baseline.Jpeg420Small, 3, new Size(2, 2), new Size(1, 1) }, + { TestImages.Jpeg.Baseline.Testorig420, 3, new Size(2, 2), new Size(1, 1) }, - // TODO: Find Ycck or Cmyk images with different subsampling - { TestImages.Jpeg.Baseline.Ycck, 4, new Size(1, 1), new Size(1, 1) }, - { TestImages.Jpeg.Baseline.Cmyk, 4, new Size(1, 1), new Size(1, 1) }, - }; + // TODO: Find Ycck or Cmyk images with different subsampling + { TestImages.Jpeg.Baseline.Ycck, 4, new Size(1, 1), new Size(1, 1) }, + { TestImages.Jpeg.Baseline.Cmyk, 4, new Size(1, 1), new Size(1, 1) }, + }; [Theory] [MemberData(nameof(ComponentVerificationData))] @@ -108,9 +108,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg Assert.Equal(componentCount, decoder.Frame.ComponentCount); Assert.Equal(componentCount, decoder.Components.Length); - JpegComponent c0 = decoder.Components[0]; - JpegComponent c1 = decoder.Components[1]; - JpegComponent c2 = decoder.Components[2]; + IJpegComponent c0 = decoder.Components[0]; + IJpegComponent c1 = decoder.Components[1]; + IJpegComponent c2 = decoder.Components[2]; var uniform1 = new Size(1, 1); @@ -126,7 +126,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg if (componentCount == 4) { - JpegComponent c3 = decoder.Components[2]; + IJpegComponent c3 = decoder.Components[2]; VerifyJpeg.VerifyComponent(c3, expectedLumaSizeInBlocks, fLuma, uniform1); } } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs index 35113f14f..c4a448ff8 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs @@ -50,7 +50,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg // Calculating data from ImageSharp byte[] sourceBytes = TestFile.Create(provider.SourceFileOrDescription).Bytes; - var decoder = new JpegDecoderCore(Configuration.Default, new JpegDecoder()); + using var decoder = new JpegDecoderCore(Configuration.Default, new JpegDecoder()); using var ms = new MemoryStream(sourceBytes); using var bufferedStream = new BufferedReadStream(Configuration.Default, ms); @@ -59,7 +59,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg var scanDecoder = new HuffmanScanDecoder(bufferedStream, debugConverter, cancellationToken: default); // This would parse entire image - decoder.ParseStream(bufferedStream, scanDecoder, cancellationToken: default); + decoder.ParseStream(bufferedStream, debugConverter, cancellationToken: default); VerifyJpeg.SaveSpectralImage(provider, debugConverter.SpectralData); } @@ -79,16 +79,15 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg // Calculating data from ImageSharp byte[] sourceBytes = TestFile.Create(provider.SourceFileOrDescription).Bytes; - var decoder = new JpegDecoderCore(Configuration.Default, new JpegDecoder()); + using var decoder = new JpegDecoderCore(Configuration.Default, new JpegDecoder()); using var ms = new MemoryStream(sourceBytes); using var bufferedStream = new BufferedReadStream(Configuration.Default, ms); // internal scan decoder which we substitute to assert spectral correctness var debugConverter = new DebugSpectralConverter(); - var scanDecoder = new HuffmanScanDecoder(bufferedStream, debugConverter, cancellationToken: default); // This would parse entire image - decoder.ParseStream(bufferedStream, scanDecoder, cancellationToken: default); + decoder.ParseStream(bufferedStream, debugConverter, cancellationToken: default); // Actual verification this.VerifySpectralCorrectnessImpl(libJpegData, debugConverter.SpectralData); @@ -195,7 +194,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg var spectralComponents = new LibJpegTools.ComponentData[frame.ComponentCount]; for (int i = 0; i < spectralComponents.Length; i++) { - JpegComponent component = frame.Components[i]; + var component = frame.Components[i] as JpegComponent; spectralComponents[i] = new LibJpegTools.ComponentData(component.WidthInBlocks, component.HeightInBlocks, component.Index); } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/SpectralToPixelConversionTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/SpectralToPixelConversionTests.cs index 0071c623c..99a31a972 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/SpectralToPixelConversionTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/SpectralToPixelConversionTests.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. using System.IO; -using System.Linq; using System.Threading; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; @@ -26,10 +25,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg TestImages.Jpeg.Baseline.MultiScanBaselineCMYK }; - public SpectralToPixelConversionTests(ITestOutputHelper output) - { - this.Output = output; - } + public SpectralToPixelConversionTests(ITestOutputHelper output) => this.Output = output; private ITestOutputHelper Output { get; } @@ -45,16 +41,16 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg // Decoding using var converter = new SpectralConverter(Configuration.Default); - var decoder = new JpegDecoderCore(Configuration.Default, new JpegDecoder()); + using var decoder = new JpegDecoderCore(Configuration.Default, new JpegDecoder()); var scanDecoder = new HuffmanScanDecoder(bufferedStream, converter, cancellationToken: default); - decoder.ParseStream(bufferedStream, scanDecoder, cancellationToken: default); + decoder.ParseStream(bufferedStream, converter, cancellationToken: default); // Test metadata provider.Utility.TestGroupName = nameof(JpegDecoderTests); provider.Utility.TestName = JpegDecoderTests.DecodeBaselineJpegOutputName; // Comparison - using (Image image = new Image(Configuration.Default, converter.GetPixelBuffer(CancellationToken.None), new ImageMetadata())) + using (var image = new Image(Configuration.Default, converter.GetPixelBuffer(CancellationToken.None), new ImageMetadata())) using (Image referenceImage = provider.GetReferenceOutputImage(appendPixelTypeToFileName: false)) { ImageSimilarityReport report = ImageComparer.Exact.CompareImagesOrFrames(referenceImage, image); diff --git a/tests/ImageSharp.Tests/Formats/Jpg/Utils/LibJpegTools.ComponentData.cs b/tests/ImageSharp.Tests/Formats/Jpg/Utils/LibJpegTools.ComponentData.cs index a390212d1..f3335fe30 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/Utils/LibJpegTools.ComponentData.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/Utils/LibJpegTools.ComponentData.cs @@ -26,11 +26,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg.Utils this.SpectralBlocks = Configuration.Default.MemoryAllocator.Allocate2D(this.WidthInBlocks, this.HeightInBlocks); } - public Size Size => new Size(this.WidthInBlocks, this.HeightInBlocks); + public byte Id { get; } + + public Size Size => new(this.WidthInBlocks, this.HeightInBlocks); public int Index { get; } - public Size SizeInBlocks => new Size(this.WidthInBlocks, this.HeightInBlocks); + public Size SizeInBlocks => new(this.WidthInBlocks, this.HeightInBlocks); public Size SamplingFactors => throw new NotSupportedException(); @@ -48,6 +50,16 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg.Utils public short MaxVal { get; private set; } = short.MinValue; + public int HorizontalSamplingFactor => throw new NotImplementedException(); + + public int VerticalSamplingFactor => throw new NotImplementedException(); + + public int DcPredictor { get; set; } + + public int DcTableId { get; set; } + + public int AcTableId { get; set; } + internal void MakeBlock(Block8x8 block, int y, int x) { block.TransposeInplace(); @@ -77,7 +89,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg.Utils } } - public void LoadSpectral(JpegComponent c) + public void LoadSpectral(IJpegComponent c) { Buffer2D data = c.SpectralBlocks; for (int y = 0; y < this.HeightInBlocks; y++) @@ -201,25 +213,19 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg.Utils return this.Equals((ComponentData)obj); } - public override int GetHashCode() - { - return HashCode.Combine(this.Index, this.HeightInBlocks, this.WidthInBlocks, this.MinVal, this.MaxVal); - } + public override int GetHashCode() => HashCode.Combine(this.Index, this.HeightInBlocks, this.WidthInBlocks, this.MinVal, this.MaxVal); - public ref Block8x8 GetBlockReference(int column, int row) - { - throw new NotImplementedException(); - } + public ref Block8x8 GetBlockReference(int column, int row) => throw new NotImplementedException(); - public static bool operator ==(ComponentData left, ComponentData right) - { - return object.Equals(left, right); - } + public void Init(int maxSubFactorH, int maxSubFactorV) => throw new NotImplementedException(); - public static bool operator !=(ComponentData left, ComponentData right) - { - return !object.Equals(left, right); - } + public void AllocateSpectral(bool fullScan) => throw new NotImplementedException(); + + public void Dispose() => throw new NotImplementedException(); + + public static bool operator ==(ComponentData left, ComponentData right) => Equals(left, right); + + public static bool operator !=(ComponentData left, ComponentData right) => !Equals(left, right); } } } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/Utils/LibJpegTools.cs b/tests/ImageSharp.Tests/Formats/Jpg/Utils/LibJpegTools.cs index b74e4445e..1d54245d3 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/Utils/LibJpegTools.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/Utils/LibJpegTools.cs @@ -56,7 +56,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg.Utils /// Executes 'dump-jpeg-coeffs.exe' for the given jpeg image file, saving the libjpeg spectral data into 'destFile'. Windows only! /// See: /// - /// https://github.com/SixLabors/Imagesharp.Tests.Images/blob/master/tools/jpeg/README.md + /// https://github.com/SixLabors/Imagesharp.Tests.Images/blob/main/tools/jpeg/README.md /// /// public static void RunDumpJpegCoeffsTool(string sourceFile, string destFile) @@ -83,7 +83,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg.Utils /// /// Extract libjpeg from the given jpg file with 'dump-jpeg-coeffs.exe'. Windows only! /// See: - /// https://github.com/SixLabors/Imagesharp.Tests.Images/blob/master/tools/jpeg/README.md + /// https://github.com/SixLabors/Imagesharp.Tests.Images/blob/main/tools/jpeg/README.md /// public static SpectralData ExtractSpectralData(string inputFile) { diff --git a/tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs index 97237bca5..eb3bc8c9a 100644 --- a/tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs @@ -11,6 +11,7 @@ using static SixLabors.ImageSharp.Tests.TestImages.Pbm; namespace SixLabors.ImageSharp.Tests.Formats.Pbm { [Trait("Format", "Pbm")] + [ValidateDisposedMemoryAllocations] public class PbmDecoderTests { [Theory] diff --git a/tests/ImageSharp.Tests/Formats/Png/Adler32Tests.cs b/tests/ImageSharp.Tests/Formats/Png/Adler32Tests.cs index 0886bd84d..77f2b7663 100644 --- a/tests/ImageSharp.Tests/Formats/Png/Adler32Tests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/Adler32Tests.cs @@ -3,6 +3,7 @@ using System; using SixLabors.ImageSharp.Compression.Zlib; +using SixLabors.ImageSharp.Tests.TestUtilities; using Xunit; using SharpAdler32 = ICSharpCode.SharpZipLib.Checksum.Adler32; @@ -15,10 +16,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png [InlineData(0)] [InlineData(1)] [InlineData(2)] - public void ReturnsCorrectWhenEmpty(uint input) - { - Assert.Equal(input, Adler32.Calculate(input, default)); - } + public void CalculateAdler_ReturnsCorrectWhenEmpty(uint input) => Assert.Equal(input, Adler32.Calculate(input, default)); [Theory] [InlineData(0)] @@ -28,24 +26,49 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png [InlineData(1024 + 15)] [InlineData(2034)] [InlineData(4096)] - public void MatchesReference(int length) + public void CalculateAdler_MatchesReference(int length) => CalculateAdlerAndCompareToReference(length); + + private static void CalculateAdlerAndCompareToReference(int length) { - var data = GetBuffer(length); + // arrange + byte[] data = GetBuffer(length); var adler = new SharpAdler32(); adler.Update(data); - long expected = adler.Value; + + // act long actual = Adler32.Calculate(data); + // assert Assert.Equal(expected, actual); } private static byte[] GetBuffer(int length) { - var data = new byte[length]; + byte[] data = new byte[length]; new Random(1).NextBytes(data); return data; } + +#if SUPPORTS_RUNTIME_INTRINSICS + [Fact] + public void RunCalculateAdlerTest_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunCalculateAdlerTest, HwIntrinsics.AllowAll); + + [Fact] + public void RunCalculateAdlerTest_WithAvxDisabled_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunCalculateAdlerTest, HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2); + + [Fact] + public void RunCalculateAdlerTest_WithoutHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunCalculateAdlerTest, HwIntrinsics.DisableHWIntrinsic); + + private static void RunCalculateAdlerTest() + { + int[] testData = { 0, 8, 215, 1024, 1024 + 15, 2034, 4096 }; + for (int i = 0; i < testData.Length; i++) + { + CalculateAdlerAndCompareToReference(testData[i]); + } + } +#endif } } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs new file mode 100644 index 000000000..edfff19a4 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs @@ -0,0 +1,179 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Formats.Png.Filters; +using SixLabors.ImageSharp.Tests.TestUtilities; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Png +{ + [Trait("Format", "Png")] + public class PngDecoderFilterTests + { + private static void RunAverageFilterTest() + { + // arrange + byte[] scanline = + { + 3, 39, 39, 39, 0, 4, 4, 4, 0, 1, 1, 1, 0, 1, 1, 1, 0, 2, 2, 2, 0, 2, 2, 2, 0, 2, 2, 2, 0, 4, 4, 4, + 0, 2, 2, 2, 0, 3, 3, 3, 0, 1, 1, 1, 0, 3, 3, 3, 0, 3, 3, 3, 0, 1, 1, 1, 0, 3, 3, 3, 0, 2, 2, 2, 0, + 1, 1, 1, 0, 3, 3, 3, 0, 1, 1, 1, 0, 3, 3, 3, 0, 1, 1, 1, 0, 3, 3, 3, 0, 3, 3, 3, 0, 254, 254, 254, + 0, 6, 6, 6, 14, 71, 71, 71, 157, 254, 254, 254, 28, 251, 251, 251, 0, 4, 4, 4, 0, 2, 2, 2, 0, 11, + 11, 11, 0, 226, 226, 226, 0, 255, 128, 234 + }; + + byte[] previousScanline = + { + 3, 74, 74, 74, 0, 73, 73, 73, 0, 73, 73, 73, 0, 74, 74, 74, 0, 74, 74, 74, 0, 73, 73, 73, 0, 72, 72, + 72, 0, 72, 72, 72, 0, 73, 73, 73, 0, 74, 74, 74, 0, 73, 73, 73, 0, 72, 72, 72, 0, 72, 72, 72, 0, 74, + 74, 74, 0, 72, 72, 72, 0, 73, 73, 73, 0, 75, 75, 75, 0, 73, 73, 73, 0, 74, 74, 74, 0, 72, 72, 72, 0, + 73, 73, 73, 0, 73, 73, 73, 0, 72, 72, 72, 0, 74, 74, 74, 0, 61, 61, 61, 0, 101, 101, 101, 78, 197, + 197, 197, 251, 152, 152, 152, 255, 155, 155, 155, 255, 162, 162, 162, 255, 175, 175, 175, 255, 160, + 160, 160, 255, 139, 128, 134 + }; + + byte[] expected = + { + 3, 76, 76, 76, 0, 78, 78, 78, 0, 76, 76, 76, 0, 76, 76, 76, 0, 77, 77, 77, 0, 77, 77, 77, 0, 76, 76, + 76, 0, 78, 78, 78, 0, 77, 77, 77, 0, 78, 78, 78, 0, 76, 76, 76, 0, 77, 77, 77, 0, 77, 77, 77, 0, 76, + 76, 76, 0, 77, 77, 77, 0, 77, 77, 77, 0, 77, 77, 77, 0, 78, 78, 78, 0, 77, 77, 77, 0, 77, 77, 77, 0, + 76, 76, 76, 0, 77, 77, 77, 0, 77, 77, 77, 0, 73, 73, 73, 0, 73, 73, 73, 14, 158, 158, 158, 203, 175, + 175, 175, 255, 158, 158, 158, 255, 160, 160, 160, 255, 163, 163, 163, 255, 180, 180, 180, 255, 140, + 140, 140, 255, 138, 6, 115 + }; + + // act + AverageFilter.Decode(scanline, previousScanline, 4); + + // assert + Assert.Equal(expected, scanline); + } + + private static void RunUpFilterTest() + { + // arrange + byte[] scanline = + { + 62, 23, 186, 150, 174, 4, 205, 59, 153, 134, 158, 86, 240, 173, 191, 58, 111, 183, 77, 37, 85, 23, + 93, 204, 110, 139, 9, 20, 87, 154, 176, 54, 207, 214, 40, 11, 179, 199, 7, 219, 174, 242, 112, 220, + 149, 5, 9, 110, 103, 107, 231, 241, 13, 70, 216, 39, 186, 237, 39, 34, 251, 185, 228, 254 + }; + + byte[] previousScanline = + { + 214, 103, 135, 26, 133, 179, 134, 168, 175, 114, 118, 99, 167, 129, 55, 105, 129, 154, 173, 235, + 179, 191, 41, 137, 253, 0, 81, 198, 159, 228, 224, 245, 14, 113, 5, 45, 126, 239, 233, 179, 229, 62, + 66, 155, 207, 117, 128, 56, 181, 190, 160, 96, 11, 248, 74, 23, 62, 253, 29, 132, 98, 192, 9, 202 + }; + + byte[] expected = + { + 62, 126, 65, 176, 51, 183, 83, 227, 72, 248, 20, 185, 151, 46, 246, 163, 240, 81, 250, 16, 8, 214, + 134, 85, 107, 139, 90, 218, 246, 126, 144, 43, 221, 71, 45, 56, 49, 182, 240, 142, 147, 48, 178, + 119, 100, 122, 137, 166, 28, 41, 135, 81, 24, 62, 34, 62, 248, 234, 68, 166, 93, 121, 237, 200 + }; + + // act + UpFilter.Decode(scanline, previousScanline); + + // assert + Assert.Equal(expected, scanline); + } + + private static void RunSubFilterTest() + { + // arrange + byte[] scanline = + { + 62, 23, 186, 150, 174, 4, 205, 59, 153, 134, 158, 86, 240, 173, 191, 58, 111, 183, 77, 37, 85, 23, + 93, 204, 110, 139, 9, 20, 87, 154, 176, 54, 207, 214, 40, 11, 179, 199, 7, 219, 174, 242, 112, 220, + 149, 5, 9, 110, 103, 107, 231, 241, 13, 70, 216, 39, 186, 237, 39, 34, 251, 185, 228, 254 + }; + + byte[] expected = + { + 62, 23, 186, 150, 174, 27, 135, 209, 71, 161, 37, 39, 55, 78, 228, 97, 166, 5, 49, 134, 251, 28, + 142, 82, 105, 167, 151, 102, 192, 65, 71, 156, 143, 23, 111, 167, 66, 222, 118, 130, 240, 208, 230, + 94, 133, 213, 239, 204, 236, 64, 214, 189, 249, 134, 174, 228, 179, 115, 213, 6, 174, 44, 185, 4 + }; + + // act + SubFilter.Decode(scanline, 4); + + // assert + Assert.Equal(expected, scanline); + } + + private static void RunPaethFilterTest() + { + // arrange + byte[] scanline = + { + 62, 23, 186, 150, 174, 4, 205, 59, 153, 134, 158, 86, 240, 173, 191, 58, 111, 183, 77, 37, 85, 23, + 93, 204, 110, 139, 9, 20, 87, 154, 176, 54, 207, 214, 40, 11, 179, 199, 7, 219, 174, 242, 112, 220, + 149, 5, 9, 110, 103, 107, 231, 241, 13, 70, 216, 39, 186, 237, 39, 34, 251, 185, 228, 254 + }; + + byte[] previousScanline = + { + 214, 103, 135, 26, 133, 179, 134, 168, 175, 114, 118, 99, 167, 129, 55, 105, 129, 154, 173, 235, + 179, 191, 41, 137, 253, 0, 81, 198, 159, 228, 224, 245, 14, 113, 5, 45, 126, 239, 233, 179, 229, 62, + 66, 155, 207, 117, 128, 56, 181, 190, 160, 96, 11, 248, 74, 23, 62, 253, 29, 132, 98, 192, 9, 202 + }; + + byte[] expected = + { + 62, 126, 65, 176, 51, 183, 14, 235, 30, 248, 172, 254, 14, 165, 53, 56, 125, 92, 250, 16, 8, 177, + 10, 220, 118, 139, 50, 240, 205, 126, 144, 43, 221, 71, 45, 54, 144, 182, 240, 142, 147, 48, 178, + 106, 40, 122, 187, 166, 143, 41, 162, 151, 24, 111, 34, 135, 248, 92, 68, 169, 243, 21, 1, 200 + }; + + // act + PaethFilter.Decode(scanline, previousScanline, 4); + + // assert + Assert.Equal(expected, scanline); + } + + [Fact] + public void AverageFilter_Works() => RunAverageFilterTest(); + + [Fact] + public void UpFilter_Works() => RunUpFilterTest(); + + [Fact] + public void SubFilter_Works() => RunSubFilterTest(); + + [Fact] + public void PaethFilter_Works() => RunPaethFilterTest(); + +#if SUPPORTS_RUNTIME_INTRINSICS + [Fact] + public void AverageFilter_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunAverageFilterTest, HwIntrinsics.AllowAll); + + [Fact] + public void AverageFilter_WithoutHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunAverageFilterTest, HwIntrinsics.DisableHWIntrinsic); + + [Fact] + public void UpFilter_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunUpFilterTest, HwIntrinsics.AllowAll); + + [Fact] + public void UpFilter_WithoutAVX2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunUpFilterTest, HwIntrinsics.DisableAVX2); + + [Fact] + public void UpFilter_WithoutHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunUpFilterTest, HwIntrinsics.DisableHWIntrinsic); + + [Fact] + public void SubFilter_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunSubFilterTest, HwIntrinsics.AllowAll); + + [Fact] + public void SubFilter_WithoutHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunSubFilterTest, HwIntrinsics.DisableHWIntrinsic); + + [Fact] + public void PaethFilter_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunPaethFilterTest, HwIntrinsics.AllowAll); + + [Fact] + public void PaethFilter_WithoutHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunPaethFilterTest, HwIntrinsics.DisableHWIntrinsic); +#endif + } +} diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs index 8b6432ac3..8f3217a58 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs @@ -75,7 +75,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png var decoder = new PngDecoder(); ImageFormatException exception = - Assert.Throws(() => decoder.Decode(Configuration.Default, memStream)); + Assert.Throws(() => decoder.Decode(Configuration.Default, memStream, default)); Assert.Equal($"CRC Error. PNG {chunkName} chunk is corrupt!", exception.Message); } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs index c29f8c589..a4fcf63ba 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs @@ -19,6 +19,7 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Formats.Png { [Trait("Format", "Png")] + [ValidateDisposedMemoryAllocations] public partial class PngDecoderTests { private const PixelTypes TestPixelTypes = PixelTypes.Rgba32 | PixelTypes.RgbaVector | PixelTypes.Argb32; @@ -111,6 +112,49 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png image.CompareToOriginal(provider, ImageComparer.Exact); } + [Theory] + [WithFile(TestImages.Png.AverageFilter3BytesPerPixel, PixelTypes.Rgba32)] + [WithFile(TestImages.Png.AverageFilter4BytesPerPixel, PixelTypes.Rgba32)] + public void Decode_WithAverageFilter(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(PngDecoder); + image.DebugSave(provider); + image.CompareToOriginal(provider, ImageComparer.Exact); + } + + [Theory] + [WithFile(TestImages.Png.SubFilter3BytesPerPixel, PixelTypes.Rgba32)] + [WithFile(TestImages.Png.SubFilter4BytesPerPixel, PixelTypes.Rgba32)] + public void Decode_WithSubFilter(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(PngDecoder); + image.DebugSave(provider); + image.CompareToOriginal(provider, ImageComparer.Exact); + } + + [Theory] + [WithFile(TestImages.Png.UpFilter, PixelTypes.Rgba32)] + public void Decode_WithUpFilter(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(PngDecoder); + image.DebugSave(provider); + image.CompareToOriginal(provider, ImageComparer.Exact); + } + + [Theory] + [WithFile(TestImages.Png.PaethFilter3BytesPerPixel, PixelTypes.Rgba32)] + [WithFile(TestImages.Png.PaethFilter4BytesPerPixel, PixelTypes.Rgba32)] + public void Decode_WithPaethFilter(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(PngDecoder); + image.DebugSave(provider); + image.CompareToOriginal(provider, ImageComparer.Exact); + } + [Theory] [WithFile(TestImages.Png.GrayA8Bit, PixelTypes.Rgba32)] [WithFile(TestImages.Png.Gray1BitTrans, PixelTypes.Rgba32)] @@ -265,6 +309,36 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png Assert.Contains("PNG Image does not contain a data chunk", ex.Message); } + [Theory] + [WithFile(TestImages.Png.Bad.MissingPaletteChunk1, PixelTypes.Rgba32)] + [WithFile(TestImages.Png.Bad.MissingPaletteChunk2, PixelTypes.Rgba32)] + public void Decode_MissingPaletteChunk_ThrowsException(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Exception ex = Record.Exception( + () => + { + using Image image = provider.GetImage(PngDecoder); + image.DebugSave(provider); + }); + Assert.NotNull(ex); + Assert.Contains("PNG Image does not contain a palette chunk", ex.Message); + } + + [Theory] + [WithFile(TestImages.Png.Bad.InvalidGammaChunk, PixelTypes.Rgba32)] + public void Decode_InvalidGammaChunk_Ignored(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Exception ex = Record.Exception( + () => + { + using Image image = provider.GetImage(PngDecoder); + image.DebugSave(provider); + }); + Assert.Null(ex); + } + [Theory] [WithFile(TestImages.Png.Bad.BitDepthZero, PixelTypes.Rgba32)] [WithFile(TestImages.Png.Bad.BitDepthThree, PixelTypes.Rgba32)] diff --git a/tests/ImageSharp.Tests/Formats/Png/PngFilterTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderFilterTests.cs similarity index 96% rename from tests/ImageSharp.Tests/Formats/Png/PngFilterTests.cs rename to tests/ImageSharp.Tests/Formats/Png/PngEncoderFilterTests.cs index 9b6119380..90fa5777b 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngFilterTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderFilterTests.cs @@ -13,7 +13,7 @@ using Xunit.Abstractions; namespace SixLabors.ImageSharp.Tests.Formats.Png { [Trait("Format", "Png")] - public partial class PngFilterTests : MeasureFixture + public class PngEncoderFilterTests : MeasureFixture { #if BENCHMARKING public const int Times = 1000000; @@ -21,7 +21,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png public const int Times = 1; #endif - public PngFilterTests(ITestOutputHelper output) + public PngEncoderFilterTests(ITestOutputHelper output) : base(output) { } @@ -39,7 +39,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png FeatureTestRunner.RunWithHwIntrinsicsFeature( RunTest, - HwIntrinsics.DisableSIMD); + HwIntrinsics.DisableHWIntrinsic); } [Fact] @@ -95,7 +95,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png FeatureTestRunner.RunWithHwIntrinsicsFeature( RunTest, - HwIntrinsics.DisableSIMD); + HwIntrinsics.DisableHWIntrinsic); } [Fact] @@ -137,7 +137,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png FeatureTestRunner.RunWithHwIntrinsicsFeature( RunTest, - HwIntrinsics.DisableSIMD); + HwIntrinsics.DisableHWIntrinsic); } [Fact] @@ -179,7 +179,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png FeatureTestRunner.RunWithHwIntrinsicsFeature( RunTest, - HwIntrinsics.DisableSIMD); + HwIntrinsics.DisableHWIntrinsic); } [Fact] diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs index 30a684702..ede4ec1cc 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.Chunks.cs @@ -302,6 +302,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png { PngChunkType.Header, PngChunkType.Gamma, + PngChunkType.EmbeddedColorProfile, PngChunkType.Palette, PngChunkType.InternationalText, PngChunkType.Text, diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index 3cc879d6b..c9b0b3c55 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -292,7 +292,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png var decoder = new PngDecoder(); - Image image = decoder.Decode(Configuration.Default, stream); + Image image = decoder.Decode(Configuration.Default, stream, default); PngMetadata metadata = image.Metadata.GetPngMetadata(); Assert.Equal(pngColorType, metadata.ColorType); diff --git a/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs index 8db1d1aaf..d0ae61fd4 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs @@ -17,7 +17,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png public class PngMetadataTests { public static readonly TheoryData RatioFiles = - new TheoryData + new() { { TestImages.Png.Splash, 11810, 11810, PixelResolutionUnit.PixelsPerMeter }, { TestImages.Png.Ratio1x4, 1, 4, PixelResolutionUnit.AspectRatio }, @@ -75,7 +75,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png input.Save(memoryStream, new PngEncoder()); memoryStream.Position = 0; - using (Image image = decoder.Decode(Configuration.Default, memoryStream)) + using (Image image = decoder.Decode(Configuration.Default, memoryStream, default)) { PngMetadata meta = image.Metadata.GetFormatMetadata(PngFormat.Instance); VerifyTextDataIsPresent(meta); @@ -123,7 +123,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png }); memoryStream.Position = 0; - using (Image image = decoder.Decode(Configuration.Default, memoryStream)) + using (Image image = decoder.Decode(Configuration.Default, memoryStream, default)) { PngMetadata meta = image.Metadata.GetFormatMetadata(PngFormat.Instance); Assert.Contains(meta.TextData, m => m.Equals(expectedText)); @@ -212,7 +212,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png using (var stream = new MemoryStream(testFile.Bytes, false)) { var decoder = new PngDecoder(); - using (Image image = decoder.Decode(Configuration.Default, stream)) + using (Image image = decoder.Decode(Configuration.Default, stream, default)) { ImageMetadata meta = image.Metadata; Assert.Equal(xResolution, meta.HorizontalResolution); @@ -222,6 +222,33 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png } } + [Theory] + [WithFile(TestImages.Png.PngWithMetadata, PixelTypes.Rgba32)] + public void Encode_PreservesColorProfile(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image input = provider.GetImage(new PngDecoder())) + { + ImageSharp.Metadata.Profiles.Icc.IccProfile expectedProfile = input.Metadata.IccProfile; + byte[] expectedProfileBytes = expectedProfile.ToByteArray(); + + using (var memStream = new MemoryStream()) + { + input.Save(memStream, new PngEncoder()); + + memStream.Position = 0; + using (var output = Image.Load(memStream)) + { + ImageSharp.Metadata.Profiles.Icc.IccProfile actualProfile = output.Metadata.IccProfile; + byte[] actualProfileBytes = actualProfile.ToByteArray(); + + Assert.NotNull(actualProfile); + Assert.Equal(expectedProfileBytes, actualProfileBytes); + } + } + } + } + [Theory] [MemberData(nameof(RatioFiles))] public void Identify_VerifyRatio(string imagePath, int xResolution, int yResolution, PixelResolutionUnit resolutionUnit) @@ -230,7 +257,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png using (var stream = new MemoryStream(testFile.Bytes, false)) { var decoder = new PngDecoder(); - IImageInfo image = decoder.Identify(Configuration.Default, stream); + IImageInfo image = decoder.Identify(Configuration.Default, stream, default); ImageMetadata meta = image.Metadata; Assert.Equal(xResolution, meta.HorizontalResolution); Assert.Equal(yResolution, meta.VerticalResolution); diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs index 55dc2ecdd..e83c5a98c 100644 --- a/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs @@ -16,6 +16,7 @@ using static SixLabors.ImageSharp.Tests.TestImages.Tga; namespace SixLabors.ImageSharp.Tests.Formats.Tga { [Trait("Format", "Tga")] + [ValidateDisposedMemoryAllocations] public class TgaDecoderTests { private static TgaDecoder TgaDecoder => new(); diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderBaseTester.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderBaseTester.cs index aeadae255..7e45edb6d 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderBaseTester.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderBaseTester.cs @@ -12,9 +12,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff { public abstract class TiffDecoderBaseTester { - protected static TiffDecoder TiffDecoder => new TiffDecoder(); + protected static TiffDecoder TiffDecoder => new(); - protected static MagickReferenceDecoder ReferenceDecoder => new MagickReferenceDecoder(); + protected static MagickReferenceDecoder ReferenceDecoder => new(); protected static void TestTiffDecoder(TestImageProvider provider, IImageDecoder referenceDecoder = null, bool useExactComparer = true, float compareTolerance = 0.001f) where TPixel : unmanaged, IPixel diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index ea0544acf..ceded79cc 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -4,6 +4,7 @@ // ReSharper disable InconsistentNaming using System; using System.IO; +using SixLabors.ImageSharp.Formats.Tiff; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; @@ -13,6 +14,7 @@ using static SixLabors.ImageSharp.Tests.TestImages.Tiff; namespace SixLabors.ImageSharp.Tests.Formats.Tiff { [Trait("Format", "Tiff")] + [ValidateDisposedMemoryAllocations] public class TiffDecoderTests : TiffDecoderBaseTester { public static readonly string[] MultiframeTestImages = Multiframes; @@ -77,9 +79,42 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [Theory] [WithFile(FlowerRgb888Planar6Strips, PixelTypes.Rgba32)] [WithFile(FlowerRgb888Planar15Strips, PixelTypes.Rgba32)] - public void TiffDecoder_Planar(TestImageProvider provider) + public void TiffDecoder_CanDecode_Planar(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] + [WithFile(Rgba8BitPlanarUnassociatedAlpha, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_Planar_32Bit(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + + [Theory] + [WithFile(Rgba16BitPlanarUnassociatedAlphaLittleEndian, PixelTypes.Rgba32)] + [WithFile(Rgba16BitPlanarUnassociatedAlphaBigEndian, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_Planar_64Bit(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + + [Theory] + [WithFile(Rgba24BitPlanarUnassociatedAlphaLittleEndian, PixelTypes.Rgba32)] + [WithFile(Rgba24BitPlanarUnassociatedAlphaBigEndian, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_Planar_96Bit(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + // Note: because the MagickReferenceDecoder fails to load the image, we only debug save them. + using Image image = provider.GetImage(); + image.DebugSave(provider); + } + + [Theory] + [WithFile(Rgba32BitPlanarUnassociatedAlphaLittleEndian, PixelTypes.Rgba32)] + [WithFile(Rgba32BitPlanarUnassociatedAlphaBigEndian, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_Planar_128Bit(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + // Note: because the MagickReferenceDecoder fails to load the image, we only debug save them. + using Image image = provider.GetImage(); + image.DebugSave(provider); + } + [Theory] [WithFile(Calliphora_PaletteUncompressed, PixelTypes.Rgba32)] [WithFile(PaletteDeflateMultistrip, PixelTypes.Rgba32)] @@ -122,6 +157,16 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff public void TiffDecoder_CanDecode_8Bit_Gray(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] + [WithFile(Rgba2BitUnassociatedAlpha, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_8Bit_WithUnassociatedAlpha(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + + [Theory] + [WithFile(FLowerRgb3Bit, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_9Bit_WithUnassociatedAlpha(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] [WithFile(Flower10BitGray, PixelTypes.Rgba32)] public void TiffDecoder_CanDecode_10Bit_Gray(TestImageProvider provider) @@ -138,11 +183,38 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff public void TiffDecoder_CanDecode_12Bit_Gray(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] + [WithFile(Rgba3BitUnassociatedAlpha, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_12Bit_WithUnassociatedAlpha(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + + [Theory] + [WithFile(Rgba3BitAssociatedAlpha, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_12Bit_WithAssociatedAlpha(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + if (TestEnvironment.IsMacOS) + { + // Only debug save on OSX: For some reason the reference image has a difference of 50%. The imagesharp output file looks ok though. + using Image image = provider.GetImage(TiffDecoder); + image.DebugSave(provider); + return; + } + + // 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); + } + [Theory] [WithFile(Flower14BitGray, PixelTypes.Rgba32)] public void TiffDecoder_CanDecode_14Bit_Gray(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] + [WithFile(FLowerRgb5Bit, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_15Bit(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] [WithFile(Flower16BitGrayLittleEndian, PixelTypes.Rgba32)] [WithFile(Flower16BitGray, PixelTypes.Rgba32)] @@ -157,6 +229,66 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff public void TiffDecoder_CanDecode_16Bit_Gray_WithPredictor(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] + [WithFile(Rgba4BitUnassociatedAlpha, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_16Bit_WithUnassociatedAlpha(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + + [Theory] + [WithFile(FLowerRgb6Bit, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_18Bit(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + + [Theory] + [WithFile(Rgba5BitUnassociatedAlpha, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_20Bit_WithUnassociatedAlpha(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + + [Theory] + [WithFile(Rgba5BitAssociatedAlpha, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_20Bit_WithAssociatedAlpha(TestImageProvider provider) + + where TPixel : unmanaged, IPixel + { + if (TestEnvironment.IsMacOS) + { + // Only debug save on OSX: For some reason the reference image has a difference of 50%. The imagesharp output file looks ok though. + using Image image = provider.GetImage(TiffDecoder); + image.DebugSave(provider); + return; + } + + // 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); + } + + [Theory] + [WithFile(FlowerRgb888Contiguous, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_24Bit(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + + [Theory] + [WithFile(Rgba6BitUnassociatedAlpha, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_24Bit_WithUnassociatedAlpha(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + + [Theory] + [WithFile(Rgba6BitAssociatedAlpha, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_24Bit_WithAssociatedAlpha(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + if (TestEnvironment.IsMacOS) + { + // Only debug save on OSX: For some reason the reference image has a difference of 50%. The imagesharp output file looks ok though. + using Image image = provider.GetImage(TiffDecoder); + image.DebugSave(provider); + return; + } + + // 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); + } + [Theory] [WithFile(Flower24BitGray, PixelTypes.Rgba32)] [WithFile(Flower24BitGrayLittleEndian, PixelTypes.Rgba32)] @@ -182,7 +314,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff where TPixel : unmanaged, IPixel { // Note: The image from MagickReferenceDecoder does not look right, maybe we are doing something wrong - // converting the pixel data from Magick.Net to our format with YCbCr? + // converting the pixel data from Magick.NET to our format with YCbCr? using Image image = provider.GetImage(); image.DebugSave(provider); } @@ -206,6 +338,34 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff image.DebugSave(provider); } + [Theory] + [WithFile(Rgba8BitUnassociatedAlpha, PixelTypes.Rgba32)] + [WithFile(Rgba8BitUnassociatedAlphaWithPredictor, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_32Bit_WithUnassociatedAlpha(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + // Note: because the MagickReferenceDecoder fails to load the image, we only debug save them. + using Image image = provider.GetImage(); + image.DebugSave(provider); + } + + [Theory] + [WithFile(Rgba8BitAssociatedAlpha, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_32Bit_WithAssociatedAlpha(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + if (TestEnvironment.IsMacOS) + { + // Only debug save on OSX: For some reason the reference image has a difference of 50%. The imagesharp output file looks ok though. + using Image image = provider.GetImage(TiffDecoder); + image.DebugSave(provider); + return; + } + + // 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); + } + [Theory] [WithFile(Flower32BitGrayPredictorBigEndian, PixelTypes.Rgba32)] [WithFile(Flower32BitGrayPredictorLittleEndian, PixelTypes.Rgba32)] @@ -222,6 +382,30 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff public void TiffDecoder_CanDecode_36Bit(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] + [WithFile(Rgba10BitUnassociatedAlphaBigEndian, PixelTypes.Rgba32)] + [WithFile(Rgba10BitUnassociatedAlphaLittleEndian, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_40Bit_WithUnassociatedAlpha(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + + [Theory] + [WithFile(Rgba10BitAssociatedAlphaBigEndian, PixelTypes.Rgba32)] + [WithFile(Rgba10BitAssociatedAlphaLittleEndian, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_40Bit_WithAssociatedAlpha(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + if (TestEnvironment.IsMacOS) + { + // Only debug save on OSX: For some reason the reference image has a difference of 50%. The imagesharp output file looks ok though. + using Image image = provider.GetImage(TiffDecoder); + image.DebugSave(provider); + return; + } + + // 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); + } + [Theory] [WithFile(FlowerRgb141414Contiguous, PixelTypes.Rgba32)] [WithFile(FlowerRgb141414Planar, PixelTypes.Rgba32)] @@ -237,12 +421,60 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff public void TiffDecoder_CanDecode_48Bit(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] + [WithFile(Rgba12BitUnassociatedAlphaBigEndian, PixelTypes.Rgba32)] + [WithFile(Rgba12BitUnassociatedAlphaLittleEndian, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_48Bit_WithUnassociatedAlpha(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + + [Theory] + [WithFile(Rgba12BitAssociatedAlphaBigEndian, PixelTypes.Rgba32)] + [WithFile(Rgba12BitAssociatedAlphaLittleEndian, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_48Bit_WithAssociatedAlpha(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + if (TestEnvironment.IsMacOS) + { + // Only debug save on OSX: For some reason the reference image has a difference of 50%. The imagesharp output file looks ok though. + using Image image = provider.GetImage(TiffDecoder); + image.DebugSave(provider); + return; + } + + // 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.0002f); + } + [Theory] [WithFile(FlowerRgb161616PredictorBigEndian, PixelTypes.Rgba32)] [WithFile(FlowerRgb161616PredictorLittleEndian, PixelTypes.Rgba32)] public void TiffDecoder_CanDecode_48Bit_WithPredictor(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] + [WithFile(Rgba14BitUnassociatedAlphaBigEndian, PixelTypes.Rgba32)] + [WithFile(Rgba14BitUnassociatedAlphaLittleEndian, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_56Bit_WithUnassociatedAlpha(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + + [Theory] + [WithFile(Rgba14BitAssociatedAlphaBigEndian, PixelTypes.Rgba32)] + [WithFile(Rgba14BitAssociatedAlphaLittleEndian, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_56Bit_WithAssociatedAlpha(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + if (TestEnvironment.IsMacOS) + { + // Only debug save on OSX: For some reason the reference image has a difference of 50%. The imagesharp output file looks ok though. + using Image image = provider.GetImage(TiffDecoder); + image.DebugSave(provider); + return; + } + + // 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.0002f); + } + [Theory] [WithFile(FlowerRgb242424Contiguous, PixelTypes.Rgba32)] [WithFile(FlowerRgb242424ContiguousLittleEndian, PixelTypes.Rgba32)] @@ -269,6 +501,17 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff image.DebugSave(provider); } + [Theory] + [WithFile(Rgba24BitUnassociatedAlphaBigEndian, PixelTypes.Rgba32)] + [WithFile(Rgba24BitUnassociatedAlphaLittleEndian, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_96Bit_WithUnassociatedAlpha(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + // Note: because the MagickReferenceDecoder fails to load the image, we only debug save them. + using Image image = provider.GetImage(); + image.DebugSave(provider); + } + [Theory] [WithFile(FlowerRgbFloat323232, PixelTypes.Rgba32)] [WithFile(FlowerRgbFloat323232LittleEndian, PixelTypes.Rgba32)] @@ -304,6 +547,27 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff image.DebugSave(provider); } + [Theory] + [WithFile(Rgba16BitUnassociatedAlphaBigEndian, PixelTypes.Rgba32)] + [WithFile(Rgba16BitUnassociatedAlphaLittleEndian, PixelTypes.Rgba32)] + [WithFile(Rgba16BitUnassociatedAlphaBigEndianWithPredictor, PixelTypes.Rgba32)] + [WithFile(Rgba16BitUnassociatedAlphaLittleEndianWithPredictor, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_128Bit_UnassociatedAlpha(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + + [Theory] + [WithFile(Rgba32BitUnassociatedAlphaBigEndian, PixelTypes.Rgba32)] + [WithFile(Rgba32BitUnassociatedAlphaLittleEndian, PixelTypes.Rgba32)] + [WithFile(Rgba32BitUnassociatedAlphaBigEndianWithPredictor, PixelTypes.Rgba32)] + [WithFile(Rgba32BitUnassociatedAlphaLittleEndianWithPredictor, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_128Bit_WithUnassociatedAlpha(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + // Note: because the MagickReferenceDecoder fails to load the image, we only debug save them. + using Image image = provider.GetImage(); + image.DebugSave(provider); + } + [Theory] [WithFile(GrayscaleDeflateMultistrip, PixelTypes.Rgba32)] [WithFile(RgbDeflateMultistrip, PixelTypes.Rgba32)] @@ -365,11 +629,23 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff public void TiffDecoder_CanDecode_PackBitsCompressed(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] + [WithFile(MultiFrameMipMap, PixelTypes.Rgba32)] + public void CanDecodeJustOneFrame(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = provider.GetImage(new TiffDecoder() { DecodingMode = FrameDecodingMode.First })) + { + Assert.Equal(1, image.Frames.Count); + } + } + [Theory] [WithFile(RgbJpegCompressed, PixelTypes.Rgba32)] [WithFile(RgbWithStripsJpegCompressed, PixelTypes.Rgba32)] [WithFile(YCbCrJpegCompressed, PixelTypes.Rgba32)] [WithFile(RgbJpegCompressedNoJpegTable, PixelTypes.Rgba32)] + [WithFile(GrayscaleJpegCompressed, PixelTypes.Rgba32)] public void TiffDecoder_CanDecode_JpegCompressed(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider, useExactComparer: false); diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs index aded52cd9..93ca611c9 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs @@ -109,6 +109,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [InlineData(TiffPhotometricInterpretation.BlackIsZero, TiffCompression.Lzw, TiffBitsPerPixel.Bit8, TiffCompression.Lzw)] [InlineData(TiffPhotometricInterpretation.PaletteColor, TiffCompression.Lzw, TiffBitsPerPixel.Bit8, TiffCompression.Lzw)] [InlineData(TiffPhotometricInterpretation.BlackIsZero, TiffCompression.CcittGroup3Fax, TiffBitsPerPixel.Bit1, TiffCompression.CcittGroup3Fax)] + [InlineData(TiffPhotometricInterpretation.BlackIsZero, TiffCompression.CcittGroup4Fax, TiffBitsPerPixel.Bit1, TiffCompression.CcittGroup4Fax)] [InlineData(TiffPhotometricInterpretation.BlackIsZero, TiffCompression.Ccitt1D, TiffBitsPerPixel.Bit1, TiffCompression.Ccitt1D)] [InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.ItuTRecT43, TiffBitsPerPixel.Bit24, TiffCompression.None)] [InlineData(TiffPhotometricInterpretation.Rgb, TiffCompression.ItuTRecT82, TiffBitsPerPixel.Bit24, TiffCompression.None)] @@ -228,8 +229,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [Theory] [WithFile(RgbUncompressed, PixelTypes.Rgba32, TiffCompression.CcittGroup3Fax, TiffCompression.CcittGroup3Fax)] + [WithFile(RgbUncompressed, PixelTypes.Rgba32, TiffCompression.CcittGroup4Fax, TiffCompression.CcittGroup4Fax)] [WithFile(RgbUncompressed, PixelTypes.Rgba32, TiffCompression.Ccitt1D, TiffCompression.Ccitt1D)] [WithFile(GrayscaleUncompressed, PixelTypes.L8, TiffCompression.CcittGroup3Fax, TiffCompression.CcittGroup3Fax)] + [WithFile(GrayscaleUncompressed, PixelTypes.L8, TiffCompression.CcittGroup4Fax, TiffCompression.CcittGroup4Fax)] [WithFile(PaletteDeflateMultistrip, PixelTypes.L8, TiffCompression.Ccitt1D, TiffCompression.Ccitt1D)] public void TiffEncoder_EncodesWithCorrectBiColorModeCompression(TestImageProvider provider, TiffCompression compression, TiffCompression expectedCompression) where TPixel : unmanaged, IPixel @@ -405,6 +408,16 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff public void TiffEncoder_EncodeBiColor_WithCcittGroup3FaxCompression_BlackIsZero_Works(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit1, TiffPhotometricInterpretation.BlackIsZero, TiffCompression.CcittGroup3Fax); + [Theory] + [WithFile(Calliphora_BiColorUncompressed, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeBiColor_WithCcittGroup4FaxCompression_WhiteIsZero_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit1, TiffPhotometricInterpretation.WhiteIsZero, TiffCompression.CcittGroup4Fax); + + [Theory] + [WithFile(Calliphora_BiColorUncompressed, PixelTypes.Rgba32)] + public void TiffEncoder_EncodeBiColor_WithCcittGroup4FaxCompression_BlackIsZero_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffEncoderCore(provider, TiffBitsPerPixel.Bit1, TiffPhotometricInterpretation.BlackIsZero, TiffCompression.CcittGroup4Fax); + [Theory] [WithFile(Calliphora_BiColorUncompressed, PixelTypes.Rgba32)] public void TiffEncoder_EncodeBiColor_WithModifiedHuffmanCompression_WhiteIsZero_Works(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs index 1c92fdf33..f29fa5d79 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs @@ -13,6 +13,7 @@ using static SixLabors.ImageSharp.Tests.TestImages.Webp; namespace SixLabors.ImageSharp.Tests.Formats.Webp { [Trait("Format", "Webp")] + [ValidateDisposedMemoryAllocations] public class WebpDecoderTests { private static WebpDecoder WebpDecoder => new(); diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs index 7043549b2..7c74429ed 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs @@ -167,18 +167,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Webp image.VerifyEncoder(provider, "webp", testOutputDetails, encoder); } - [Theory] - [WithFile(TestPatternOpaque, PixelTypes.Rgba32)] - [WithFile(TestPatternOpaqueSmall, PixelTypes.Rgba32)] - public void Encode_Lossless_WorksWithTestPattern(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - using Image image = provider.GetImage(); - - var encoder = new WebpEncoder() { FileFormat = WebpFileFormatType.Lossless }; - image.VerifyEncoder(provider, "webp", string.Empty, encoder); - } - [Fact] public void Encode_Lossless_OneByOnePixel_Works() { @@ -279,6 +267,34 @@ namespace SixLabors.ImageSharp.Tests.Formats.Webp image.VerifyEncoder(provider, "webp", testOutputDetails, encoder, customComparer: GetComparer(quality)); } + [Theory] + [WithFile(TestImages.Png.Transparency, PixelTypes.Rgba32, false)] + [WithFile(TestImages.Png.Transparency, PixelTypes.Rgba32, true)] + public void Encode_Lossy_WithAlpha_Works(TestImageProvider provider, bool compressed) + where TPixel : unmanaged, IPixel + { + var encoder = new WebpEncoder() + { + FileFormat = WebpFileFormatType.Lossy, + UseAlphaCompression = compressed + }; + + using Image image = provider.GetImage(); + image.VerifyEncoder(provider, "webp", $"with_alpha_compressed_{compressed}", encoder, ImageComparer.Tolerant(0.04f)); + } + + [Theory] + [WithFile(TestPatternOpaque, PixelTypes.Rgba32)] + [WithFile(TestPatternOpaqueSmall, PixelTypes.Rgba32)] + public void Encode_Lossless_WorksWithTestPattern(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + + var encoder = new WebpEncoder() { FileFormat = WebpFileFormatType.Lossless }; + image.VerifyEncoder(provider, "webp", string.Empty, encoder); + } + [Theory] [WithFile(TestPatternOpaque, PixelTypes.Rgba32)] [WithFile(TestPatternOpaqueSmall, PixelTypes.Rgba32)] diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs index 7fba86b4f..d1f148be0 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs @@ -1,7 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using System; using System.IO; +using System.Threading.Tasks; using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.PixelFormats; @@ -66,7 +68,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Webp [Theory] [WithFile(TestImages.Webp.Lossy.WithXmp, PixelTypes.Rgba32, false)] [WithFile(TestImages.Webp.Lossy.WithXmp, PixelTypes.Rgba32, true)] - public async void IgnoreMetadata_ControlsWhetherXmpIsParsed(TestImageProvider provider, bool ignoreMetadata) + public async Task IgnoreMetadata_ControlsWhetherXmpIsParsed(TestImageProvider provider, bool ignoreMetadata) where TPixel : unmanaged, IPixel { var decoder = new WebpDecoder { IgnoreMetadata = ignoreMetadata }; @@ -149,5 +151,48 @@ namespace SixLabors.ImageSharp.Tests.Formats.Webp Assert.NotNull(actualExif); Assert.Equal(expectedExif.Values.Count, actualExif.Values.Count); } + + [Theory] + [WithFile(TestImages.Webp.Lossy.WithIccp, PixelTypes.Rgba32, WebpFileFormatType.Lossless)] + [WithFile(TestImages.Webp.Lossy.WithIccp, PixelTypes.Rgba32, WebpFileFormatType.Lossy)] + public void Encode_PreservesColorProfile(TestImageProvider provider, WebpFileFormatType fileFormat) + where TPixel : unmanaged, IPixel + { + using (Image input = provider.GetImage(new WebpDecoder())) + { + ImageSharp.Metadata.Profiles.Icc.IccProfile expectedProfile = input.Metadata.IccProfile; + byte[] expectedProfileBytes = expectedProfile.ToByteArray(); + + using (var memStream = new MemoryStream()) + { + input.Save(memStream, new WebpEncoder() + { + FileFormat = fileFormat + }); + + memStream.Position = 0; + using (var output = Image.Load(memStream)) + { + ImageSharp.Metadata.Profiles.Icc.IccProfile actualProfile = output.Metadata.IccProfile; + byte[] actualProfileBytes = actualProfile.ToByteArray(); + + Assert.NotNull(actualProfile); + Assert.Equal(expectedProfileBytes, actualProfileBytes); + } + } + } + } + + [Theory] + [WithFile(TestImages.Webp.Lossy.WithExifNotEnoughData, PixelTypes.Rgba32)] + public void WebpDecoder_IgnoresInvalidExifChunk(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Exception ex = Record.Exception(() => + { + using Image image = provider.GetImage(); + }); + Assert.Null(ex); + } } } diff --git a/tests/ImageSharp.Tests/Helpers/RuntimeEnvironmentTests.cs b/tests/ImageSharp.Tests/Helpers/RuntimeEnvironmentTests.cs index 8074b8b15..5246f8f91 100644 --- a/tests/ImageSharp.Tests/Helpers/RuntimeEnvironmentTests.cs +++ b/tests/ImageSharp.Tests/Helpers/RuntimeEnvironmentTests.cs @@ -28,7 +28,7 @@ namespace SixLabors.ImageSharp.Tests.Helpers { Assert.True(RuntimeEnvironment.IsOSPlatform(OSPlatform.Linux)); } - else if (TestEnvironment.IsOSX) + else if (TestEnvironment.IsMacOS) { Assert.True(RuntimeEnvironment.IsOSPlatform(OSPlatform.OSX)); } diff --git a/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs b/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs index 00a178c8f..6bcb7e7ad 100644 --- a/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs +++ b/tests/ImageSharp.Tests/IO/ChunkedMemoryStreamTests.cs @@ -18,19 +18,19 @@ namespace SixLabors.ImageSharp.Tests.IO /// public class ChunkedMemoryStreamTests { - private readonly MemoryAllocator allocator; + /// + /// The default length in bytes of each buffer chunk when allocating large buffers. + /// + private const int DefaultLargeChunkSize = 1024 * 1024 * 4; // 4 Mb - public ChunkedMemoryStreamTests() - { - this.allocator = Configuration.Default.MemoryAllocator; - } + /// + /// The default length in bytes of each buffer chunk when allocating small buffers. + /// + private const int DefaultSmallChunkSize = DefaultLargeChunkSize / 32; // 128 Kb - [Fact] - public void MemoryStream_Ctor_InvalidCapacities() - { - Assert.Throws(() => new ChunkedMemoryStream(int.MinValue, this.allocator)); - Assert.Throws(() => new ChunkedMemoryStream(0, this.allocator)); - } + private readonly MemoryAllocator allocator; + + public ChunkedMemoryStreamTests() => this.allocator = Configuration.Default.MemoryAllocator; [Fact] public void MemoryStream_GetPositionTest_Negative() @@ -61,11 +61,11 @@ namespace SixLabors.ImageSharp.Tests.IO } [Theory] - [InlineData(ChunkedMemoryStream.DefaultBufferLength)] - [InlineData((int)(ChunkedMemoryStream.DefaultBufferLength * 1.5))] - [InlineData(ChunkedMemoryStream.DefaultBufferLength * 4)] - [InlineData((int)(ChunkedMemoryStream.DefaultBufferLength * 5.5))] - [InlineData(ChunkedMemoryStream.DefaultBufferLength * 8)] + [InlineData(DefaultSmallChunkSize)] + [InlineData((int)(DefaultSmallChunkSize * 1.5))] + [InlineData(DefaultSmallChunkSize * 4)] + [InlineData((int)(DefaultSmallChunkSize * 5.5))] + [InlineData(DefaultSmallChunkSize * 16)] public void MemoryStream_ReadByteTest(int length) { using MemoryStream ms = this.CreateTestStream(length); @@ -73,7 +73,7 @@ namespace SixLabors.ImageSharp.Tests.IO ms.CopyTo(cms); cms.Position = 0; - var expected = ms.ToArray(); + byte[] expected = ms.ToArray(); for (int i = 0; i < expected.Length; i++) { @@ -82,11 +82,11 @@ namespace SixLabors.ImageSharp.Tests.IO } [Theory] - [InlineData(ChunkedMemoryStream.DefaultBufferLength)] - [InlineData((int)(ChunkedMemoryStream.DefaultBufferLength * 1.5))] - [InlineData(ChunkedMemoryStream.DefaultBufferLength * 4)] - [InlineData((int)(ChunkedMemoryStream.DefaultBufferLength * 5.5))] - [InlineData(ChunkedMemoryStream.DefaultBufferLength * 8)] + [InlineData(DefaultSmallChunkSize)] + [InlineData((int)(DefaultSmallChunkSize * 1.5))] + [InlineData(DefaultSmallChunkSize * 4)] + [InlineData((int)(DefaultSmallChunkSize * 5.5))] + [InlineData(DefaultSmallChunkSize * 16)] public void MemoryStream_ReadByteBufferTest(int length) { using MemoryStream ms = this.CreateTestStream(length); @@ -94,8 +94,8 @@ namespace SixLabors.ImageSharp.Tests.IO ms.CopyTo(cms); cms.Position = 0; - var expected = ms.ToArray(); - var buffer = new byte[2]; + byte[] expected = ms.ToArray(); + byte[] buffer = new byte[2]; for (int i = 0; i < expected.Length; i += 2) { cms.Read(buffer); @@ -105,11 +105,11 @@ namespace SixLabors.ImageSharp.Tests.IO } [Theory] - [InlineData(ChunkedMemoryStream.DefaultBufferLength)] - [InlineData((int)(ChunkedMemoryStream.DefaultBufferLength * 1.5))] - [InlineData(ChunkedMemoryStream.DefaultBufferLength * 4)] - [InlineData((int)(ChunkedMemoryStream.DefaultBufferLength * 5.5))] - [InlineData(ChunkedMemoryStream.DefaultBufferLength * 8)] + [InlineData(DefaultSmallChunkSize)] + [InlineData((int)(DefaultSmallChunkSize * 1.5))] + [InlineData(DefaultSmallChunkSize * 4)] + [InlineData((int)(DefaultSmallChunkSize * 5.5))] + [InlineData(DefaultSmallChunkSize * 16)] public void MemoryStream_ReadByteBufferSpanTest(int length) { using MemoryStream ms = this.CreateTestStream(length); @@ -117,7 +117,7 @@ namespace SixLabors.ImageSharp.Tests.IO ms.CopyTo(cms); cms.Position = 0; - var expected = ms.ToArray(); + byte[] expected = ms.ToArray(); Span buffer = new byte[2]; for (int i = 0; i < expected.Length; i += 2) { @@ -257,24 +257,24 @@ namespace SixLabors.ImageSharp.Tests.IO public void MemoryStream_CopyTo_Invalid() { ChunkedMemoryStream memoryStream; - const string BufferSize = "bufferSize"; + const string bufferSize = nameof(bufferSize); using (memoryStream = new ChunkedMemoryStream(this.allocator)) { - const string Destination = "destination"; - Assert.Throws(Destination, () => memoryStream.CopyTo(destination: null)); + const string destination = nameof(destination); + Assert.Throws(destination, () => memoryStream.CopyTo(destination: null)); // Validate the destination parameter first. - Assert.Throws(Destination, () => memoryStream.CopyTo(destination: null, bufferSize: 0)); - Assert.Throws(Destination, () => memoryStream.CopyTo(destination: null, bufferSize: -1)); + Assert.Throws(destination, () => memoryStream.CopyTo(destination: null, bufferSize: 0)); + Assert.Throws(destination, () => memoryStream.CopyTo(destination: null, bufferSize: -1)); // Then bufferSize. - Assert.Throws(BufferSize, () => memoryStream.CopyTo(Stream.Null, bufferSize: 0)); // 0-length buffer doesn't make sense. - Assert.Throws(BufferSize, () => memoryStream.CopyTo(Stream.Null, bufferSize: -1)); + Assert.Throws(bufferSize, () => memoryStream.CopyTo(Stream.Null, bufferSize: 0)); // 0-length buffer doesn't make sense. + Assert.Throws(bufferSize, () => memoryStream.CopyTo(Stream.Null, bufferSize: -1)); } // After the Stream is disposed, we should fail on all CopyTos. - Assert.Throws(BufferSize, () => memoryStream.CopyTo(Stream.Null, bufferSize: 0)); // Not before bufferSize is validated. - Assert.Throws(BufferSize, () => memoryStream.CopyTo(Stream.Null, bufferSize: -1)); + Assert.Throws(bufferSize, () => memoryStream.CopyTo(Stream.Null, bufferSize: 0)); // Not before bufferSize is validated. + Assert.Throws(bufferSize, () => memoryStream.CopyTo(Stream.Null, bufferSize: -1)); ChunkedMemoryStream disposedStream = memoryStream; @@ -369,7 +369,7 @@ namespace SixLabors.ImageSharp.Tests.IO private MemoryStream CreateTestStream(int length) { - var buffer = new byte[length]; + byte[] buffer = new byte[length]; var random = new Random(); random.NextBytes(buffer); diff --git a/tests/ImageSharp.Tests/Image/ImageTests.Decode_Cancellation.cs b/tests/ImageSharp.Tests/Image/ImageTests.Decode_Cancellation.cs index 317a5129c..831913844 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.Decode_Cancellation.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.Decode_Cancellation.cs @@ -124,7 +124,7 @@ namespace SixLabors.ImageSharp.Tests this.continueSemaphore.Release(); } - protected override Stream CreateStream() => this.TestFormat.CreateAsyncSamaphoreStream(this.notifyWaitPositionReachedSemaphore, this.continueSemaphore, this.isTestStreamSeekable); + protected override Stream CreateStream() => this.TestFormat.CreateAsyncSemaphoreStream(this.notifyWaitPositionReachedSemaphore, this.continueSemaphore, this.isTestStreamSeekable); } } } diff --git a/tests/ImageSharp.Tests/Image/ImageTests.ImageLoadTestBase.cs b/tests/ImageSharp.Tests/Image/ImageTests.ImageLoadTestBase.cs index 44d7daa74..9992c3055 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.ImageLoadTestBase.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.ImageLoadTestBase.cs @@ -63,12 +63,11 @@ namespace SixLabors.ImageSharp.Tests this.localImageFormatMock = new Mock(); var detector = new Mock(); - detector.Setup(x => x.Identify(It.IsAny(), It.IsAny())).Returns(this.localImageInfoMock.Object); - detector.Setup(x => x.IdentifyAsync(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(this.localImageInfoMock.Object); + detector.Setup(x => x.Identify(It.IsAny(), It.IsAny(), It.IsAny())).Returns(this.localImageInfoMock.Object); this.localDecoder = detector.As(); - this.localDecoder.Setup(x => x.Decode(It.IsAny(), It.IsAny())) - .Callback((c, s) => + this.localDecoder.Setup(x => x.Decode(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((c, s, ct) => { using (var ms = new MemoryStream()) { @@ -78,8 +77,8 @@ namespace SixLabors.ImageSharp.Tests }) .Returns(this.localStreamReturnImageRgba32); - this.localDecoder.Setup(x => x.Decode(It.IsAny(), It.IsAny())) - .Callback((c, s) => + this.localDecoder.Setup(x => x.Decode(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((c, s, ct) => { using (var ms = new MemoryStream()) { diff --git a/tests/ImageSharp.Tests/Image/ImageTests.LoadPixelData.cs b/tests/ImageSharp.Tests/Image/ImageTests.LoadPixelData.cs index a4044b906..7683ee688 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.LoadPixelData.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.LoadPixelData.cs @@ -14,6 +14,7 @@ namespace SixLabors.ImageSharp.Tests [Theory] [InlineData(false)] [InlineData(true)] + [ValidateDisposedMemoryAllocations] public void FromPixels(bool useSpan) { Rgba32[] data = { Color.Black, Color.White, Color.White, Color.Black, }; diff --git a/tests/ImageSharp.Tests/Image/ImageTests.Load_FileSystemPath_PassLocalConfiguration.cs b/tests/ImageSharp.Tests/Image/ImageTests.Load_FileSystemPath_PassLocalConfiguration.cs index 72477a832..0d5eead4b 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.Load_FileSystemPath_PassLocalConfiguration.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.Load_FileSystemPath_PassLocalConfiguration.cs @@ -41,7 +41,7 @@ namespace SixLabors.ImageSharp.Tests var img = Image.Load(this.TopLevelConfiguration, this.MockFilePath, this.localDecoder.Object); Assert.NotNull(img); - this.localDecoder.Verify(x => x.Decode(this.TopLevelConfiguration, this.DataStream)); + this.localDecoder.Verify(x => x.Decode(this.TopLevelConfiguration, this.DataStream, default)); } [Fact] @@ -50,7 +50,7 @@ namespace SixLabors.ImageSharp.Tests var img = Image.Load(this.TopLevelConfiguration, this.MockFilePath, this.localDecoder.Object); Assert.NotNull(img); - this.localDecoder.Verify(x => x.Decode(this.TopLevelConfiguration, this.DataStream)); + this.localDecoder.Verify(x => x.Decode(this.TopLevelConfiguration, this.DataStream, default)); } [Fact] diff --git a/tests/ImageSharp.Tests/Image/ImageTests.Load_FromStream_PassLocalConfiguration.cs b/tests/ImageSharp.Tests/Image/ImageTests.Load_FromStream_PassLocalConfiguration.cs index 17b557f83..b08a82523 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.Load_FromStream_PassLocalConfiguration.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.Load_FromStream_PassLocalConfiguration.cs @@ -65,7 +65,7 @@ namespace SixLabors.ImageSharp.Tests var img = Image.Load(this.TopLevelConfiguration, stream, this.localDecoder.Object); Assert.NotNull(img); - this.localDecoder.Verify(x => x.Decode(this.TopLevelConfiguration, stream)); + this.localDecoder.Verify(x => x.Decode(this.TopLevelConfiguration, stream, default)); } [Fact] @@ -75,7 +75,7 @@ namespace SixLabors.ImageSharp.Tests var img = Image.Load(this.TopLevelConfiguration, stream, this.localDecoder.Object); Assert.NotNull(img); - this.localDecoder.Verify(x => x.Decode(this.TopLevelConfiguration, stream)); + this.localDecoder.Verify(x => x.Decode(this.TopLevelConfiguration, stream, default)); } [Fact] diff --git a/tests/ImageSharp.Tests/Image/LargeImageIntegrationTests.cs b/tests/ImageSharp.Tests/Image/LargeImageIntegrationTests.cs index b2ee9d673..ce1f902e5 100644 --- a/tests/ImageSharp.Tests/Image/LargeImageIntegrationTests.cs +++ b/tests/ImageSharp.Tests/Image/LargeImageIntegrationTests.cs @@ -2,7 +2,10 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.IO; using Microsoft.DotNet.RemoteExecutor; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; @@ -32,9 +35,42 @@ namespace SixLabors.ImageSharp.Tests Configuration configuration = Configuration.Default.Clone(); configuration.PreferContiguousImageBuffers = true; - using var image = new Image(configuration, 8192, 4096); + using var image = new Image(configuration, 2048, 2048); Assert.True(image.DangerousTryGetSinglePixelMemory(out Memory mem)); - Assert.Equal(8192 * 4096, mem.Length); + Assert.Equal(2048 * 2048, mem.Length); + } + } + + [Theory] + [InlineData("bmp")] + [InlineData("png")] + [InlineData("jpeg")] + [InlineData("gif")] + [InlineData("tiff")] + [InlineData("webp")] + public void PreferContiguousImageBuffers_LoadImage_BufferIsContiguous(string formatOuter) + { + // Run remotely to avoid large allocation in the test process: + RemoteExecutor.Invoke(RunTest, formatOuter).Dispose(); + + static void RunTest(string formatInner) + { + using IDisposable mem = MemoryAllocatorValidator.MonitorAllocations(); + + Configuration configuration = Configuration.Default.Clone(); + configuration.PreferContiguousImageBuffers = true; + IImageEncoder encoder = configuration.ImageFormatsManager.FindEncoder( + configuration.ImageFormatsManager.FindFormatByFileExtension(formatInner)); + string dir = TestEnvironment.CreateOutputDirectory(".Temp"); + string path = Path.Combine(dir, $"{Guid.NewGuid().ToString()}.{formatInner}"); + using (Image temp = new(2048, 2048)) + { + temp.Save(path, encoder); + } + + using var image = Image.Load(configuration, path); + File.Delete(path); + Assert.Equal(1, image.GetPixelMemoryGroup().Count); } } diff --git a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj index 28c778787..a4f1de17b 100644 --- a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj +++ b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj @@ -6,23 +6,18 @@ SixLabors.ImageSharp.Tests AnyCPU;x64;x86 SixLabors.ImageSharp.Tests - Debug;Release;Debug-InnerLoop;Release-InnerLoop + Debug;Release - net6.0;net5.0;netcoreapp3.1;netcoreapp2.1;net472 - - - - - netcoreapp3.1 + net7.0;net6.0 - net5.0;netcoreapp3.1;netcoreapp2.1;net472 + net6.0 @@ -47,7 +42,6 @@ - diff --git a/tests/ImageSharp.Tests/Memory/Allocators/MemoryDiagnosticsTests.cs b/tests/ImageSharp.Tests/Memory/Allocators/MemoryDiagnosticsTests.cs new file mode 100644 index 000000000..5fab655cb --- /dev/null +++ b/tests/ImageSharp.Tests/Memory/Allocators/MemoryDiagnosticsTests.cs @@ -0,0 +1,122 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using Microsoft.DotNet.RemoteExecutor; +using SixLabors.ImageSharp.Diagnostics; +using SixLabors.ImageSharp.Memory; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Memory.Allocators +{ + public class MemoryDiagnosticsTests + { + private const int OneMb = 1 << 20; + + private static MemoryAllocator Allocator => Configuration.Default.MemoryAllocator; + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void PerfectCleanup_NoLeaksReported(bool isGroupOuter) + { + RemoteExecutor.Invoke(RunTest, isGroupOuter.ToString()).Dispose(); + + static void RunTest(string isGroupInner) + { + bool isGroup = bool.Parse(isGroupInner); + int leakCounter = 0; + MemoryDiagnostics.UndisposedAllocation += _ => Interlocked.Increment(ref leakCounter); + + List buffers = new(); + + Assert.Equal(0, MemoryDiagnostics.TotalUndisposedAllocationCount); + for (int length = 1024; length <= 64 * OneMb; length *= 2) + { + long cntBefore = MemoryDiagnostics.TotalUndisposedAllocationCount; + IDisposable buffer = isGroup ? + Allocator.AllocateGroup(length, 1024) : + Allocator.Allocate(length); + buffers.Add(buffer); + long cntAfter = MemoryDiagnostics.TotalUndisposedAllocationCount; + Assert.True(cntAfter > cntBefore); + } + + foreach (IDisposable buffer in buffers) + { + long cntBefore = MemoryDiagnostics.TotalUndisposedAllocationCount; + buffer.Dispose(); + long cntAfter = MemoryDiagnostics.TotalUndisposedAllocationCount; + Assert.True(cntAfter < cntBefore); + } + + Assert.Equal(0, MemoryDiagnostics.TotalUndisposedAllocationCount); + Assert.Equal(0, leakCounter); + } + } + + [Theory] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + public void MissingCleanup_LeaksAreReported(bool isGroupOuter, bool subscribeLeakHandleOuter) + { + RemoteExecutor.Invoke(RunTest, isGroupOuter.ToString(), subscribeLeakHandleOuter.ToString()).Dispose(); + + static void RunTest(string isGroupInner, string subscribeLeakHandleInner) + { + bool isGroup = bool.Parse(isGroupInner); + bool subscribeLeakHandle = bool.Parse(subscribeLeakHandleInner); + int leakCounter = 0; + bool stackTraceOk = true; + if (subscribeLeakHandle) + { + MemoryDiagnostics.UndisposedAllocation += stackTrace => + { + Interlocked.Increment(ref leakCounter); + stackTraceOk &= stackTrace.Contains(nameof(RunTest)) && stackTrace.Contains(nameof(AllocateAndForget)); + Assert.Contains(nameof(AllocateAndForget), stackTrace); + }; + } + + Assert.Equal(0, MemoryDiagnostics.TotalUndisposedAllocationCount); + for (int length = 1024; length <= 64 * OneMb; length *= 2) + { + long cntBefore = MemoryDiagnostics.TotalUndisposedAllocationCount; + AllocateAndForget(length, isGroup); + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + long cntAfter = MemoryDiagnostics.TotalUndisposedAllocationCount; + Assert.True(cntAfter > cntBefore); + } + + if (subscribeLeakHandle) + { + // Make sure at least some of the leak callbacks have time to complete on the ThreadPool + Thread.Sleep(200); + Assert.True(leakCounter > 3, $"leakCounter did not count enough leaks ({leakCounter} only)"); + } + + Assert.True(stackTraceOk); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void AllocateAndForget(int length, bool isGroup) + { + if (isGroup) + { + _ = Allocator.AllocateGroup(length, 1024); + } + else + { + _ = Allocator.Allocate(length); + } + } + } + } +} diff --git a/tests/ImageSharp.Tests/Memory/Allocators/RefCountedLifetimeGuardTests.cs b/tests/ImageSharp.Tests/Memory/Allocators/RefCountedLifetimeGuardTests.cs index 7fb3b7b7b..4b808e863 100644 --- a/tests/ImageSharp.Tests/Memory/Allocators/RefCountedLifetimeGuardTests.cs +++ b/tests/ImageSharp.Tests/Memory/Allocators/RefCountedLifetimeGuardTests.cs @@ -110,7 +110,7 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators } } - private class MockLifetimeGuard : RefCountedLifetimeGuard + private class MockLifetimeGuard : RefCountedMemoryLifetimeGuard { public int ReleaseInvocationCount { get; private set; } diff --git a/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedMemoryPoolTests.Trim.cs b/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedMemoryPoolTests.Trim.cs index f748b7feb..d1a8e25bb 100644 --- a/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedMemoryPoolTests.Trim.cs +++ b/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedMemoryPoolTests.Trim.cs @@ -58,11 +58,11 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators [Collection(nameof(NonParallelCollection))] public class NonParallel { - public static readonly bool IsNotMacOs = !TestEnvironment.IsOSX; + public static readonly bool IsNotMacOS = !TestEnvironment.IsMacOS; - // TODO: Investigate failures on MacOS. All handles are released after GC. + // TODO: Investigate failures on macOS. All handles are released after GC. // (It seems to happen more consistently on .NET 6.) - [ConditionalFact(nameof(IsNotMacOs))] + [ConditionalFact(nameof(IsNotMacOS))] public void MultiplePoolInstances_TrimPeriodElapsed_AllAreTrimmed() { if (!TestEnvironment.RunsOnCI) diff --git a/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedMemoryPoolTests.cs b/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedMemoryPoolTests.cs index 4ab2c93a5..2976bf7fd 100644 --- a/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedMemoryPoolTests.cs +++ b/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedMemoryPoolTests.cs @@ -245,7 +245,10 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators cleanup.Register(b1); } - [Theory] + public static readonly bool IsNotMacOS = !TestEnvironment.IsMacOS; + + // TODO: Investigate macOS failures + [ConditionalTheory(nameof(IsNotMacOS))] [InlineData(false)] [InlineData(true)] public void RentReturnRelease_SubsequentRentReturnsDifferentHandles(bool multiple) diff --git a/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs b/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs index 45a7cc278..0520c3c1f 100644 --- a/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs +++ b/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs @@ -259,9 +259,9 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators [InlineData(1200)] // Group of two UniformUnmanagedMemoryPool buffers public void AllocateMemoryGroup_Finalization_ReturnsToPool(int length) { - if (TestEnvironment.IsOSX) + if (TestEnvironment.IsMacOS) { - // Skip on OSX: https://github.com/SixLabors/ImageSharp/issues/1887 + // Skip on macOS: https://github.com/SixLabors/ImageSharp/issues/1887 return; } @@ -321,9 +321,9 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators [InlineData(600)] // Group of single UniformUnmanagedMemoryPool buffer public void AllocateSingleMemoryOwner_Finalization_ReturnsToPool(int length) { - if (TestEnvironment.IsOSX) + if (TestEnvironment.IsMacOS) { - // Skip on OSX: https://github.com/SixLabors/ImageSharp/issues/1887 + // Skip on macOS: https://github.com/SixLabors/ImageSharp/issues/1887 return; } @@ -379,5 +379,20 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators g1.GetSpan()[0] = 42; } } + +#if NETCOREAPP3_1_OR_GREATER + [Fact] + public void Issue2001_NegativeMemoryReportedByGc() + { + RemoteExecutor.Invoke(RunTest).Dispose(); + + static void RunTest() + { + // Emulate GC.GetGCMemoryInfo() issue https://github.com/dotnet/runtime/issues/65466 + UniformUnmanagedMemoryPoolMemoryAllocator.GetTotalAvailableMemoryBytes = () => -402354176; + _ = MemoryAllocator.Create(); + } + } +#endif } } diff --git a/tests/ImageSharp.Tests/Memory/Buffer2DTests.SwapOrCopyContent.cs b/tests/ImageSharp.Tests/Memory/Buffer2DTests.SwapOrCopyContent.cs index f58136f73..5b6938df1 100644 --- a/tests/ImageSharp.Tests/Memory/Buffer2DTests.SwapOrCopyContent.cs +++ b/tests/ImageSharp.Tests/Memory/Buffer2DTests.SwapOrCopyContent.cs @@ -13,13 +13,13 @@ namespace SixLabors.ImageSharp.Tests.Memory { public class SwapOrCopyContent { - private readonly TestMemoryAllocator MemoryAllocator = new TestMemoryAllocator(); + private readonly TestMemoryAllocator memoryAllocator = new TestMemoryAllocator(); [Fact] public void SwapOrCopyContent_WhenBothAllocated() { - using (Buffer2D a = this.MemoryAllocator.Allocate2D(10, 5, AllocationOptions.Clean)) - using (Buffer2D b = this.MemoryAllocator.Allocate2D(3, 7, AllocationOptions.Clean)) + using (Buffer2D a = this.memoryAllocator.Allocate2D(10, 5, AllocationOptions.Clean)) + using (Buffer2D b = this.memoryAllocator.Allocate2D(3, 7, AllocationOptions.Clean)) { a[1, 3] = 666; b[1, 3] = 444; @@ -46,7 +46,7 @@ namespace SixLabors.ImageSharp.Tests.Memory using var destData = MemoryGroup.Wrap(new int[100]); using var dest = new Buffer2D(destData, 10, 10); - using (Buffer2D source = this.MemoryAllocator.Allocate2D(10, 10, AllocationOptions.Clean)) + using (Buffer2D source = this.memoryAllocator.Allocate2D(10, 10, AllocationOptions.Clean)) { source[0, 0] = 1; dest[0, 0] = 2; @@ -68,9 +68,9 @@ namespace SixLabors.ImageSharp.Tests.Memory [Fact] public void WhenBothAreMemoryOwners_ShouldSwap() { - this.MemoryAllocator.BufferCapacityInBytes = sizeof(int) * 50; - using Buffer2D a = this.MemoryAllocator.Allocate2D(48, 2); - using Buffer2D b = this.MemoryAllocator.Allocate2D(50, 2); + this.memoryAllocator.BufferCapacityInBytes = sizeof(int) * 50; + using Buffer2D a = this.memoryAllocator.Allocate2D(48, 2); + using Buffer2D b = this.memoryAllocator.Allocate2D(50, 2); Memory a0 = a.FastMemoryGroup[0]; Memory a1 = a.FastMemoryGroup[1]; @@ -90,8 +90,8 @@ namespace SixLabors.ImageSharp.Tests.Memory [Fact] public void WhenBothAreMemoryOwners_ShouldReplaceViews() { - using Buffer2D a = this.MemoryAllocator.Allocate2D(100, 1); - using Buffer2D b = this.MemoryAllocator.Allocate2D(100, 2); + using Buffer2D a = this.memoryAllocator.Allocate2D(100, 1); + using Buffer2D b = this.memoryAllocator.Allocate2D(100, 2); a.FastMemoryGroup[0].Span[42] = 1; b.FastMemoryGroup[0].Span[33] = 2; @@ -121,7 +121,7 @@ namespace SixLabors.ImageSharp.Tests.Memory using var destOwner = new TestMemoryManager(data); using var dest = new Buffer2D(MemoryGroup.Wrap(destOwner.Memory), 21, 1); - using Buffer2D source = this.MemoryAllocator.Allocate2D(21, 1); + using Buffer2D source = this.memoryAllocator.Allocate2D(21, 1); source.FastMemoryGroup[0].Span[10] = color; @@ -145,7 +145,7 @@ namespace SixLabors.ImageSharp.Tests.Memory using var destOwner = new TestMemoryManager(data); using var dest = new Buffer2D(MemoryGroup.Wrap(destOwner.Memory), 21, 1); - using Buffer2D source = this.MemoryAllocator.Allocate2D(22, 1); + using Buffer2D source = this.memoryAllocator.Allocate2D(22, 1); source.FastMemoryGroup[0].Span[10] = color; diff --git a/tests/ImageSharp.Tests/MemoryAllocatorValidator.cs b/tests/ImageSharp.Tests/MemoryAllocatorValidator.cs new file mode 100644 index 000000000..13664ee9b --- /dev/null +++ b/tests/ImageSharp.Tests/MemoryAllocatorValidator.cs @@ -0,0 +1,77 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Threading; +using SixLabors.ImageSharp.Diagnostics; +using Xunit; + +namespace SixLabors.ImageSharp.Tests +{ + public static class MemoryAllocatorValidator + { + private static readonly AsyncLocal LocalInstance = new(); + + public static bool MonitoringAllocations => LocalInstance.Value != null; + + static MemoryAllocatorValidator() + { + MemoryDiagnostics.MemoryAllocated += MemoryDiagnostics_MemoryAllocated; + MemoryDiagnostics.MemoryReleased += MemoryDiagnostics_MemoryReleased; + } + + private static void MemoryDiagnostics_MemoryReleased() + { + TestMemoryDiagnostics backing = LocalInstance.Value; + if (backing != null) + { + backing.TotalRemainingAllocated--; + } + } + + private static void MemoryDiagnostics_MemoryAllocated() + { + TestMemoryDiagnostics backing = LocalInstance.Value; + if (backing != null) + { + backing.TotalAllocated++; + backing.TotalRemainingAllocated++; + } + } + + public static TestMemoryDiagnostics MonitorAllocations() + { + var diag = new TestMemoryDiagnostics(); + LocalInstance.Value = diag; + return diag; + } + + public static void StopMonitoringAllocations() => LocalInstance.Value = null; + + public static void ValidateAllocations(int expectedAllocationCount = 0) + => LocalInstance.Value?.Validate(expectedAllocationCount); + + public class TestMemoryDiagnostics : IDisposable + { + public int TotalAllocated { get; set; } + + public int TotalRemainingAllocated { get; set; } + + public void Validate(int expectedAllocationCount) + { + var count = this.TotalRemainingAllocated; + var pass = expectedAllocationCount == count; + Assert.True(pass, $"Expected a {expectedAllocationCount} undisposed buffers but found {count}"); + } + + public void Dispose() + { + this.Validate(0); + if (LocalInstance.Value == this) + { + StopMonitoringAllocations(); + } + } + } + } +} diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs index 7fc3ff6f1..1668b7a12 100644 --- a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs +++ b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs @@ -51,6 +51,9 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif { ExifTag.ImageDescription, "ImageDescription" }, { ExifTag.ExposureTime, new Rational(1.0 / 1600.0) }, { ExifTag.Model, "Model" }, + { ExifTag.XPAuthor, "The XPAuthor text" }, + { ExifTag.UserComment, new EncodedString(EncodedString.CharacterCode.Unicode, "The Unicode text") }, + { ExifTag.GPSAreaInformation, new EncodedString("Default constructor text (GPSAreaInformation)") }, }; [Theory] @@ -351,10 +354,15 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif TestProfile(profile); - using Image thumbnail = profile.CreateThumbnail(); + using Image thumbnail = profile.CreateThumbnail(); Assert.NotNull(thumbnail); Assert.Equal(256, thumbnail.Width); Assert.Equal(170, thumbnail.Height); + + using Image genericThumbnail = profile.CreateThumbnail(); + Assert.NotNull(genericThumbnail); + Assert.Equal(256, genericThumbnail.Width); + Assert.Equal(170, genericThumbnail.Height); } [Fact] @@ -504,7 +512,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif public void IfdStructure() { var exif = new ExifProfile(); - exif.SetValue(ExifTag.XPAuthor, Encoding.GetEncoding("UCS-2").GetBytes("Dan Petitt")); + exif.SetValue(ExifTag.XPAuthor, "Dan Petitt"); Span actualBytes = exif.ToByteArray(); diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/Values/ExifValuesTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/Values/ExifValuesTests.cs index a5ca115d4..aaad4e230 100644 --- a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/Values/ExifValuesTests.cs +++ b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/Values/ExifValuesTests.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using System; +using System.Text; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using Xunit; @@ -23,11 +25,6 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif.Values { ExifTag.XMP }, { ExifTag.CFAPattern2 }, { ExifTag.TIFFEPStandardID }, - { ExifTag.XPTitle }, - { ExifTag.XPComment }, - { ExifTag.XPAuthor }, - { ExifTag.XPKeywords }, - { ExifTag.XPSubject }, { ExifTag.GPSVersionID }, }; @@ -295,7 +292,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif.Values { ExifTag.GPSDestLongitudeRef }, { ExifTag.GPSDestBearingRef }, { ExifTag.GPSDestDistanceRef }, - { ExifTag.GPSDateStamp } + { ExifTag.GPSDateStamp }, }; public static TheoryData UndefinedTags => new TheoryData @@ -311,7 +308,6 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif.Values { ExifTag.ExifVersion }, { ExifTag.ComponentsConfiguration }, { ExifTag.MakerNote }, - { ExifTag.UserComment }, { ExifTag.FlashpixVersion }, { ExifTag.SpatialFrequencyResponse }, { ExifTag.SpatialFrequencyResponse2 }, @@ -319,10 +315,24 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif.Values { ExifTag.CFAPattern }, { ExifTag.DeviceSettingDescription }, { ExifTag.ImageSourceData }, + }; + + public static TheoryData EncodedStringTags => new TheoryData + { + { ExifTag.UserComment }, { ExifTag.GPSProcessingMethod }, { ExifTag.GPSAreaInformation } }; + public static TheoryData Ucs2StringTags => new TheoryData + { + { ExifTag.XPTitle }, + { ExifTag.XPComment }, + { ExifTag.XPAuthor }, + { ExifTag.XPKeywords }, + { ExifTag.XPSubject }, + }; + [Theory] [MemberData(nameof(ByteTags))] public void ExifByteTests(ExifTag tag) @@ -593,5 +603,46 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif.Values var typed = (ExifByteArray)value; Assert.Equal(expected, typed.Value); } + + [Theory] + [MemberData(nameof(EncodedStringTags))] + public void ExifEncodedStringTests(ExifTag tag) + { + foreach (object code in Enum.GetValues(typeof(EncodedString.CharacterCode))) + { + var charCode = (EncodedString.CharacterCode)code; + + Assert.Equal(ExifEncodedStringHelpers.CharacterCodeBytesLength, ExifEncodedStringHelpers.GetCodeBytes(charCode).Length); + + const string expectedText = "test string"; + var expected = new EncodedString(charCode, expectedText); + ExifValue value = ExifValues.Create(tag); + + Assert.False(value.TrySetValue(123)); + Assert.True(value.TrySetValue(expected)); + + var typed = (ExifEncodedString)value; + Assert.Equal(expected, typed.Value); + Assert.Equal(expectedText, (string)typed.Value); + Assert.Equal(charCode, typed.Value.Code); + } + } + + [Theory] + [MemberData(nameof(Ucs2StringTags))] + public void ExifUcs2StringTests(ExifTag tag) + { + const string expected = "Dan Petitt"; + ExifValue value = ExifValues.Create(tag); + + Assert.False(value.TrySetValue(123)); + Assert.True(value.TrySetValue(expected)); + + var typed = (ExifUcs2String)value; + Assert.Equal(expected, typed.Value); + + Assert.True(value.TrySetValue(Encoding.GetEncoding("UCS-2").GetBytes(expected))); + Assert.Equal(expected, typed.Value); + } } } diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/XMP/XmpProfileTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/XMP/XmpProfileTests.cs index 81dad699a..a3c15d0b3 100644 --- a/tests/ImageSharp.Tests/Metadata/Profiles/XMP/XmpProfileTests.cs +++ b/tests/ImageSharp.Tests/Metadata/Profiles/XMP/XmpProfileTests.cs @@ -4,6 +4,7 @@ using System; using System.IO; using System.Text; +using System.Threading.Tasks; using System.Xml.Linq; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Gif; @@ -31,7 +32,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Xmp [Theory] [WithFile(TestImages.Gif.Receipt, PixelTypes.Rgba32)] - public async void ReadXmpMetadata_FromGif_Works(TestImageProvider provider) + public async Task ReadXmpMetadata_FromGif_Works(TestImageProvider provider) where TPixel : unmanaged, IPixel { using (Image image = await provider.GetImageAsync(GifDecoder)) @@ -45,7 +46,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Xmp [WithFile(TestImages.Jpeg.Baseline.Lake, PixelTypes.Rgba32)] [WithFile(TestImages.Jpeg.Baseline.Metadata, PixelTypes.Rgba32)] [WithFile(TestImages.Jpeg.Baseline.ExtendedXmp, PixelTypes.Rgba32)] - public async void ReadXmpMetadata_FromJpg_Works(TestImageProvider provider) + public async Task ReadXmpMetadata_FromJpg_Works(TestImageProvider provider) where TPixel : unmanaged, IPixel { using (Image image = await provider.GetImageAsync(JpegDecoder)) @@ -57,7 +58,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Xmp [Theory] [WithFile(TestImages.Png.XmpColorPalette, PixelTypes.Rgba32)] - public async void ReadXmpMetadata_FromPng_Works(TestImageProvider provider) + public async Task ReadXmpMetadata_FromPng_Works(TestImageProvider provider) where TPixel : unmanaged, IPixel { using (Image image = await provider.GetImageAsync(PngDecoder)) @@ -69,7 +70,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Xmp [Theory] [WithFile(TestImages.Tiff.SampleMetadata, PixelTypes.Rgba32)] - public async void ReadXmpMetadata_FromTiff_Works(TestImageProvider provider) + public async Task ReadXmpMetadata_FromTiff_Works(TestImageProvider provider) where TPixel : unmanaged, IPixel { using (Image image = await provider.GetImageAsync(TiffDecoder)) @@ -81,7 +82,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Xmp [Theory] [WithFile(TestImages.Webp.Lossy.WithXmp, PixelTypes.Rgba32)] - public async void ReadXmpMetadata_FromWebp_Works(TestImageProvider provider) + public async Task ReadXmpMetadata_FromWebp_Works(TestImageProvider provider) where TPixel : unmanaged, IPixel { using (Image image = await provider.GetImageAsync(WebpDecoder)) @@ -157,7 +158,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Xmp } [Fact] - public async void WritingJpeg_PreservesExtendedXmpProfile() + public async Task WritingJpeg_PreservesExtendedXmpProfile() { // arrange var provider = TestImageProvider.File(TestImages.Jpeg.Baseline.ExtendedXmp); diff --git a/tests/ImageSharp.Tests/PixelFormats/PixelOperations/PixelOperationsTests.cs b/tests/ImageSharp.Tests/PixelFormats/PixelOperations/PixelOperationsTests.cs index 2a89c1d39..3e1b22e85 100644 --- a/tests/ImageSharp.Tests/PixelFormats/PixelOperations/PixelOperationsTests.cs +++ b/tests/ImageSharp.Tests/PixelFormats/PixelOperations/PixelOperationsTests.cs @@ -320,30 +320,51 @@ namespace SixLabors.ImageSharp.Tests.PixelFormats.PixelOperations (s, d) => this.Operations.ToVector4(this.Configuration, s, d.GetSpan())); } - public static readonly TheoryData Generic_To_Data = new TheoryData + public static readonly TheoryData Generic_To_Data = new() { + new TestPixel(), new TestPixel(), - new TestPixel(), + new TestPixel(), + new TestPixel(), + new TestPixel(), new TestPixel(), - new TestPixel(), - new TestPixel(), + new TestPixel(), + new TestPixel(), + new TestPixel(), + new TestPixel(), + new TestPixel(), + new TestPixel(), new TestPixel(), + new TestPixel(), + new TestPixel(), + new TestPixel(), + new TestPixel(), + new TestPixel(), + new TestPixel(), + new TestPixel(), + new TestPixel(), + new TestPixel(), new TestPixel(), - new TestPixel() + new TestPixel(), + new TestPixel(), + new TestPixel(), + new TestPixel(), + new TestPixel(), + new TestPixel(), }; [Theory] [MemberData(nameof(Generic_To_Data))] - public void Generic_To(TestPixel dummy) + public void Generic_To(TestPixel _) where TDestPixel : unmanaged, IPixel { - const int Count = 2134; - TPixel[] source = CreatePixelTestData(Count); - var expected = new TDestPixel[Count]; + const int count = 2134; + TPixel[] source = CreatePixelTestData(count); + var expected = new TDestPixel[count]; PixelConverterTests.ReferenceImplementations.To(this.Configuration, source, expected); - TestOperation(source, expected, (s, d) => this.Operations.To(this.Configuration, s, d.GetSpan())); + TestOperation(source, expected, (s, d) => this.Operations.To(this.Configuration, s, d.GetSpan()), false); } [Theory] @@ -1234,23 +1255,11 @@ namespace SixLabors.ImageSharp.Tests.PixelFormats.PixelOperations } // TODO: We really need a PixelTypeInfo.BitsPerComponent property!! - private static bool IsComplexPixel() + private static bool IsComplexPixel() => default(TDest) switch { - switch (default(TDest)) - { - case HalfSingle _: - case HalfVector2 _: - case L16 _: - case La32 _: - case NormalizedShort2 _: - case Rg32 _: - case Short2 _: - return true; - - default: - return Unsafe.SizeOf() > sizeof(int); - } - } + HalfSingle or HalfVector2 or L16 or La32 or NormalizedShort2 or Rg32 or Short2 => true, + _ => Unsafe.SizeOf() > sizeof(int), + }; } } } diff --git a/tests/ImageSharp.Tests/PixelFormats/RgbaVectorTests.cs b/tests/ImageSharp.Tests/PixelFormats/RgbaVectorTests.cs index 438d5f2ed..bfd901b95 100644 --- a/tests/ImageSharp.Tests/PixelFormats/RgbaVectorTests.cs +++ b/tests/ImageSharp.Tests/PixelFormats/RgbaVectorTests.cs @@ -189,5 +189,21 @@ namespace SixLabors.ImageSharp.Tests.PixelFormats // assert Assert.Equal(expected, rgba.ToScaledVector4()); } + + [Fact] + public void Issue2048() + { + // https://github.com/SixLabors/ImageSharp/issues/2048 + RgbaVector green = Color.Green.ToPixel(); + using Image source = new(Configuration.Default, 1, 1, green); + using Image clone = source.CloneAs(); + + Rgba32 srcColor = default; + Rgba32 cloneColor = default; + source[0, 0].ToRgba32(ref srcColor); + clone[0, 0].ToRgba32(ref cloneColor); + + Assert.Equal(srcColor, cloneColor); + } } } diff --git a/tests/ImageSharp.Tests/Processing/Convolution/KernelSamplingMapTest.cs b/tests/ImageSharp.Tests/Processing/Convolution/KernelSamplingMapTest.cs new file mode 100644 index 000000000..64e1ea2cb --- /dev/null +++ b/tests/ImageSharp.Tests/Processing/Convolution/KernelSamplingMapTest.cs @@ -0,0 +1,421 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Processing.Processors.Convolution; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Processing.Convolution +{ + [Trait("Category", "Processors")] + public class KernelSamplingMapTest + { + [Fact] + public void KernalSamplingMap_Kernel5Image7x7RepeatBorder() + { + var kernelSize = new Size(5, 5); + var bounds = new Rectangle(0, 0, 7, 7); + var mode = BorderWrappingMode.Repeat; + int[] expected = + { + 0, 0, 0, 1, 2, + 0, 0, 1, 2, 3, + 0, 1, 2, 3, 4, + 1, 2, 3, 4, 5, + 2, 3, 4, 5, 6, + 3, 4, 5, 6, 6, + 4, 5, 6, 6, 6, + }; + this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected); + } + + [Fact] + public void KernalSamplingMap_Kernel5Image7x7BounceBorder() + { + var kernelSize = new Size(5, 5); + var bounds = new Rectangle(0, 0, 7, 7); + var mode = BorderWrappingMode.Bounce; + int[] expected = + { + 2, 1, 0, 1, 2, + 1, 0, 1, 2, 3, + 0, 1, 2, 3, 4, + 1, 2, 3, 4, 5, + 2, 3, 4, 5, 6, + 3, 4, 5, 6, 5, + 4, 5, 6, 5, 4, + }; + this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected); + } + + [Fact] + public void KernalSamplingMap_Kernel5Image7x7MirrorBorder() + { + var kernelSize = new Size(5, 5); + var bounds = new Rectangle(0, 0, 7, 7); + var mode = BorderWrappingMode.Mirror; + int[] expected = + { + 1, 0, 0, 1, 2, + 0, 0, 1, 2, 3, + 0, 1, 2, 3, 4, + 1, 2, 3, 4, 5, + 2, 3, 4, 5, 6, + 3, 4, 5, 6, 6, + 4, 5, 6, 6, 5, + }; + this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected); + } + + [Fact] + public void KernalSamplingMap_Kernel5Image7x7WrapBorder() + { + var kernelSize = new Size(5, 5); + var bounds = new Rectangle(0, 0, 7, 7); + var mode = BorderWrappingMode.Wrap; + int[] expected = + { + 5, 6, 0, 1, 2, + 6, 0, 1, 2, 3, + 0, 1, 2, 3, 4, + 1, 2, 3, 4, 5, + 2, 3, 4, 5, 6, + 3, 4, 5, 6, 0, + 4, 5, 6, 0, 1, + }; + this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected); + } + + [Fact] + public void KernalSamplingMap_Kernel5Image9x9BounceBorder() + { + var kernelSize = new Size(5, 5); + var bounds = new Rectangle(1, 1, 9, 9); + var mode = BorderWrappingMode.Bounce; + int[] expected = + { + 3, 2, 1, 2, 3, + 2, 1, 2, 3, 4, + 1, 2, 3, 4, 5, + 2, 3, 4, 5, 6, + 3, 4, 5, 6, 7, + 4, 5, 6, 7, 8, + 5, 6, 7, 8, 9, + 6, 7, 8, 9, 8, + 7, 8, 9, 8, 7, + }; + this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected); + } + + [Fact] + public void KernalSamplingMap_Kernel5Image9x9MirrorBorder() + { + var kernelSize = new Size(5, 5); + var bounds = new Rectangle(1, 1, 9, 9); + var mode = BorderWrappingMode.Mirror; + int[] expected = + { + 2, 1, 1, 2, 3, + 1, 1, 2, 3, 4, + 1, 2, 3, 4, 5, + 2, 3, 4, 5, 6, + 3, 4, 5, 6, 7, + 4, 5, 6, 7, 8, + 5, 6, 7, 8, 9, + 6, 7, 8, 9, 9, + 7, 8, 9, 9, 8, + }; + this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected); + } + + [Fact] + public void KernalSamplingMap_Kernel5Image9x9WrapBorder() + { + var kernelSize = new Size(5, 5); + var bounds = new Rectangle(1, 1, 9, 9); + var mode = BorderWrappingMode.Wrap; + int[] expected = + { + 8, 9, 1, 2, 3, + 9, 1, 2, 3, 4, + 1, 2, 3, 4, 5, + 2, 3, 4, 5, 6, + 3, 4, 5, 6, 7, + 4, 5, 6, 7, 8, + 5, 6, 7, 8, 9, + 6, 7, 8, 9, 1, + 7, 8, 9, 1, 2, + }; + this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected); + } + + [Fact] + public void KernalSamplingMap_Kernel5Image7x7RepeatBorderTile() + { + var kernelSize = new Size(5, 5); + var bounds = new Rectangle(2, 2, 7, 7); + var mode = BorderWrappingMode.Repeat; + int[] expected = + { + 2, 2, 2, 3, 4, + 2, 2, 3, 4, 5, + 2, 3, 4, 5, 6, + 3, 4, 5, 6, 7, + 4, 5, 6, 7, 8, + 5, 6, 7, 8, 8, + 6, 7, 8, 8, 8, + }; + this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected); + } + + [Fact] + public void KernalSamplingMap_Kernel5Image7x7BounceBorderTile() + { + var kernelSize = new Size(5, 5); + var bounds = new Rectangle(2, 2, 7, 7); + var mode = BorderWrappingMode.Bounce; + int[] expected = + { + 4, 3, 2, 3, 4, + 3, 2, 3, 4, 5, + 2, 3, 4, 5, 6, + 3, 4, 5, 6, 7, + 4, 5, 6, 7, 8, + 5, 6, 7, 8, 7, + 6, 7, 8, 7, 6, + }; + this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected); + } + + [Fact] + public void KernalSamplingMap_Kernel5Image7x7MirrorBorderTile() + { + var kernelSize = new Size(5, 5); + var bounds = new Rectangle(2, 2, 7, 7); + var mode = BorderWrappingMode.Mirror; + int[] expected = + { + 3, 2, 2, 3, 4, + 2, 2, 3, 4, 5, + 2, 3, 4, 5, 6, + 3, 4, 5, 6, 7, + 4, 5, 6, 7, 8, + 5, 6, 7, 8, 8, + 6, 7, 8, 8, 7, + }; + this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected); + } + + [Fact] + public void KernalSamplingMap_Kernel5Image7x7WrapBorderTile() + { + var kernelSize = new Size(5, 5); + var bounds = new Rectangle(2, 2, 7, 7); + var mode = BorderWrappingMode.Wrap; + int[] expected = + { + 7, 8, 2, 3, 4, + 8, 2, 3, 4, 5, + 2, 3, 4, 5, 6, + 3, 4, 5, 6, 7, + 4, 5, 6, 7, 8, + 5, 6, 7, 8, 2, + 6, 7, 8, 2, 3, + }; + this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected); + } + + [Fact] + public void KernalSamplingMap_Kernel3Image7x7RepeatBorder() + { + var kernelSize = new Size(3, 3); + var bounds = new Rectangle(0, 0, 7, 7); + var mode = BorderWrappingMode.Repeat; + int[] expected = + { + 0, 0, 1, + 0, 1, 2, + 1, 2, 3, + 2, 3, 4, + 3, 4, 5, + 4, 5, 6, + 5, 6, 6, + }; + this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected); + } + + [Fact] + public void KernalSamplingMap_Kernel3Image7x7BounceBorder() + { + var kernelSize = new Size(3, 3); + var bounds = new Rectangle(0, 0, 7, 7); + var mode = BorderWrappingMode.Bounce; + int[] expected = + { + 1, 0, 1, + 0, 1, 2, + 1, 2, 3, + 2, 3, 4, + 3, 4, 5, + 4, 5, 6, + 5, 6, 5, + }; + this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected); + } + + [Fact] + public void KernalSamplingMap_Kernel3Image7x7MirrorBorder() + { + var kernelSize = new Size(3, 3); + var bounds = new Rectangle(0, 0, 7, 7); + var mode = BorderWrappingMode.Mirror; + int[] expected = + { + 0, 0, 1, + 0, 1, 2, + 1, 2, 3, + 2, 3, 4, + 3, 4, 5, + 4, 5, 6, + 5, 6, 6, + }; + this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected); + } + + [Fact] + public void KernalSamplingMap_Kernel3Image7x7WrapBorder() + { + var kernelSize = new Size(3, 3); + var bounds = new Rectangle(0, 0, 7, 7); + var mode = BorderWrappingMode.Wrap; + int[] expected = + { + 6, 0, 1, + 0, 1, 2, + 1, 2, 3, + 2, 3, 4, + 3, 4, 5, + 4, 5, 6, + 5, 6, 0, + }; + this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected); + } + + [Fact] + public void KernalSamplingMap_Kernel3Image7x7RepeatBorderTile() + { + var kernelSize = new Size(3, 3); + var bounds = new Rectangle(2, 2, 7, 7); + var mode = BorderWrappingMode.Repeat; + int[] expected = + { + 2, 2, 3, + 2, 3, 4, + 3, 4, 5, + 4, 5, 6, + 5, 6, 7, + 6, 7, 8, + 7, 8, 8, + }; + this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected); + } + + [Fact] + public void KernalSamplingMap_Kernel3Image7BounceBorderTile() + { + var kernelSize = new Size(3, 3); + var bounds = new Rectangle(2, 2, 7, 7); + var mode = BorderWrappingMode.Bounce; + int[] expected = + { + 3, 2, 3, + 2, 3, 4, + 3, 4, 5, + 4, 5, 6, + 5, 6, 7, + 6, 7, 8, + 7, 8, 7, + }; + this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected); + } + + [Fact] + public void KernalSamplingMap_Kernel3Image7MirrorBorderTile() + { + var kernelSize = new Size(3, 3); + var bounds = new Rectangle(2, 2, 7, 7); + var mode = BorderWrappingMode.Mirror; + int[] expected = + { + 2, 2, 3, + 2, 3, 4, + 3, 4, 5, + 4, 5, 6, + 5, 6, 7, + 6, 7, 8, + 7, 8, 8, + }; + this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected); + } + + [Fact] + public void KernalSamplingMap_Kernel3Image7x7WrapBorderTile() + { + var kernelSize = new Size(3, 3); + var bounds = new Rectangle(2, 2, 7, 7); + var mode = BorderWrappingMode.Wrap; + int[] expected = + { + 8, 2, 3, + 2, 3, 4, + 3, 4, 5, + 4, 5, 6, + 5, 6, 7, + 6, 7, 8, + 7, 8, 2, + }; + this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected); + } + + [Fact] + public void KernalSamplingMap_Kernel3Image7x5WrapBorderTile() + { + var kernelSize = new Size(3, 3); + var bounds = new Rectangle(2, 2, 7, 5); + var mode = BorderWrappingMode.Wrap; + int[] xExpected = + { + 8, 2, 3, + 2, 3, 4, + 3, 4, 5, + 4, 5, 6, + 5, 6, 7, + 6, 7, 8, + 7, 8, 2, + }; + int[] yExpected = + { + 6, 2, 3, + 2, 3, 4, + 3, 4, 5, + 4, 5, 6, + 5, 6, 2, + }; + this.AssertOffsets(kernelSize, bounds, mode, mode, xExpected, yExpected); + } + + private void AssertOffsets(Size kernelSize, Rectangle bounds, BorderWrappingMode xBorderMode, BorderWrappingMode yBorderMode, int[] xExpected, int[] yExpected) + { + // Arrange + var map = new KernelSamplingMap(Configuration.Default.MemoryAllocator); + + // Act + map.BuildSamplingOffsetMap(kernelSize.Height, kernelSize.Width, bounds, xBorderMode, yBorderMode); + + // Assert + var xOffsets = map.GetColumnOffsetSpan().ToArray(); + Assert.Equal(xExpected, xOffsets); + var yOffsets = map.GetRowOffsetSpan().ToArray(); + Assert.Equal(yExpected, yOffsets); + } + } +} diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/AutoOrientTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/AutoOrientTests.cs index 38fde5060..597c02dfc 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/AutoOrientTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/AutoOrientTests.cs @@ -18,42 +18,41 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms public const string FlipTestFile = TestImages.Bmp.F; public static readonly TheoryData InvalidOrientationValues - = new TheoryData - { - { ExifDataType.Byte, new byte[] { 1 } }, - { ExifDataType.SignedByte, new byte[] { 2 } }, - { ExifDataType.SignedShort, BitConverter.GetBytes((short)3) }, - { ExifDataType.Long, BitConverter.GetBytes(4U) }, - { ExifDataType.SignedLong, BitConverter.GetBytes(5) } - }; + = new() + { + { ExifDataType.Byte, new byte[] { 1 } }, + { ExifDataType.SignedByte, new byte[] { 2 } }, + { ExifDataType.SignedShort, BitConverter.GetBytes((short)3) }, + { ExifDataType.Long, BitConverter.GetBytes(4U) }, + { ExifDataType.SignedLong, BitConverter.GetBytes(5) } + }; - public static readonly TheoryData ExifOrientationValues = new TheoryData - { - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8 - }; + public static readonly TheoryData ExifOrientationValues + = new() + { + ExifOrientationMode.Unknown, + ExifOrientationMode.TopLeft, + ExifOrientationMode.TopRight, + ExifOrientationMode.BottomRight, + ExifOrientationMode.BottomLeft, + ExifOrientationMode.LeftTop, + ExifOrientationMode.RightTop, + ExifOrientationMode.RightBottom, + ExifOrientationMode.LeftBottom + }; [Theory] [WithFile(FlipTestFile, nameof(ExifOrientationValues), PixelTypes.Rgba32)] public void AutoOrient_WorksForAllExifOrientations(TestImageProvider provider, ushort orientation) where TPixel : unmanaged, IPixel { - using (Image image = provider.GetImage()) - { - image.Metadata.ExifProfile = new ExifProfile(); - image.Metadata.ExifProfile.SetValue(ExifTag.Orientation, orientation); + using Image image = provider.GetImage(); + image.Metadata.ExifProfile = new ExifProfile(); + image.Metadata.ExifProfile.SetValue(ExifTag.Orientation, orientation); - image.Mutate(x => x.AutoOrient()); - image.DebugSave(provider, orientation, appendPixelTypeToFileName: false); - image.CompareToReferenceOutput(provider, orientation, appendPixelTypeToFileName: false); - } + image.Mutate(x => x.AutoOrient()); + image.DebugSave(provider, orientation, appendPixelTypeToFileName: false); + image.CompareToReferenceOutput(provider, orientation, appendPixelTypeToFileName: false); } [Theory] @@ -76,19 +75,17 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms // Change the number of components bytes[20] = 1; - var orientationCodeData = new byte[8]; + byte[] orientationCodeData = new byte[8]; Array.Copy(orientation, orientationCodeData, orientation.Length); ulong orientationCode = BitConverter.ToUInt64(orientationCodeData, 0); - using (Image image = provider.GetImage()) - using (Image reference = image.Clone()) - { - image.Metadata.ExifProfile = new ExifProfile(bytes); - image.Mutate(x => x.AutoOrient()); - image.DebugSave(provider, $"{dataType}-{orientationCode}", appendPixelTypeToFileName: false); - ImageComparer.Exact.VerifySimilarity(image, reference); - } + using Image image = provider.GetImage(); + using Image reference = image.Clone(); + image.Metadata.ExifProfile = new ExifProfile(bytes); + image.Mutate(x => x.AutoOrient()); + image.DebugSave(provider, $"{dataType}-{orientationCode}", appendPixelTypeToFileName: false); + ImageComparer.Exact.VerifySimilarity(image, reference); } } } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/PadTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/PadTest.cs index b1441d109..780758c2b 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/PadTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/PadTest.cs @@ -20,41 +20,37 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms public void ImageShouldPad(TestImageProvider provider) where TPixel : unmanaged, IPixel { - using (Image image = provider.GetImage()) - { - image.Mutate(x => x.Pad(image.Width + 50, image.Height + 50)); - image.DebugSave(provider); + using Image image = provider.GetImage(); + image.Mutate(x => x.Pad(image.Width + 50, image.Height + 50)); + image.DebugSave(provider); - // Check pixels are empty - for (int y = 0; y < 25; y++) + // Check pixels are empty + for (int y = 0; y < 25; y++) + { + for (int x = 0; x < 25; x++) { - for (int x = 0; x < 25; x++) - { - Assert.Equal(default, image[x, y]); - } + Assert.Equal(default, image[x, y]); } } } [Theory] - [WithFileCollection(nameof(CommonTestImages), PixelTypes.Rgba32)] + [WithFileCollection(nameof(CommonTestImages), PixelTypes.Rgba32 | PixelTypes.Rgb24)] public void ImageShouldPadWithBackgroundColor(TestImageProvider provider) where TPixel : unmanaged, IPixel { - var color = Color.Red; + Color color = Color.Red; TPixel expected = color.ToPixel(); - using (Image image = provider.GetImage()) - { - image.Mutate(x => x.Pad(image.Width + 50, image.Height + 50, color)); - image.DebugSave(provider); + using Image image = provider.GetImage(); + image.Mutate(x => x.Pad(image.Width + 50, image.Height + 50, color)); + image.DebugSave(provider); - // Check pixels are filled - for (int y = 0; y < 25; y++) + // Check pixels are filled + for (int y = 0; y < 25; y++) + { + for (int x = 0; x < 25; x++) { - for (int x = 0; x < 25; x++) - { - Assert.Equal(expected, image[x, y]); - } + Assert.Equal(expected, image[x, y]); } } } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs index 6e275b824..9d766e058 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs @@ -34,7 +34,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms }; private static readonly ImageComparer ValidatorComparer = - ImageComparer.TolerantPercentage(TestEnvironment.IsOSX && TestEnvironment.RunsOnCI ? 0.26F : 0.07F); + ImageComparer.TolerantPercentage(TestEnvironment.IsMacOS && TestEnvironment.RunsOnCI ? 0.26F : 0.07F); [Fact] public void Resize_PixelAgnostic() @@ -472,7 +472,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms var options = new ResizeOptions { Size = new Size(image.Width + 200, image.Height + 200), - Mode = ResizeMode.BoxPad + Mode = ResizeMode.BoxPad, + PadColor = Color.HotPink }; image.Mutate(x => x.Resize(options)); @@ -580,7 +581,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms var options = new ResizeOptions { Size = new Size(image.Width + 200, image.Height), - Mode = ResizeMode.Pad + Mode = ResizeMode.Pad, + PadColor = Color.Lavender }; image.Mutate(x => x.Resize(options)); diff --git a/tests/ImageSharp.Tests/Processing/Transforms/PadTest.cs b/tests/ImageSharp.Tests/Processing/Transforms/PadTest.cs index 227e470d4..3e6726ba0 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/PadTest.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/PadTest.cs @@ -23,7 +23,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms Assert.Equal(width, resizeProcessor.DestinationWidth); Assert.Equal(height, resizeProcessor.DestinationHeight); - Assert.Equal(sampler, resizeProcessor.Sampler); + Assert.Equal(sampler, resizeProcessor.Options.Sampler); } } } diff --git a/tests/ImageSharp.Tests/Processing/Transforms/ResizeTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/ResizeTests.cs index 60f7aaa0b..a29f4c035 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/ResizeTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/ResizeTests.cs @@ -35,7 +35,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms Assert.Equal(width, resizeProcessor.DestinationWidth); Assert.Equal(height, resizeProcessor.DestinationHeight); - Assert.Equal(sampler, resizeProcessor.Sampler); + Assert.Equal(sampler, resizeProcessor.Options.Sampler); } [Fact] @@ -52,8 +52,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms Assert.Equal(width, resizeProcessor.DestinationWidth); Assert.Equal(height, resizeProcessor.DestinationHeight); - Assert.Equal(sampler, resizeProcessor.Sampler); - Assert.Equal(compand, resizeProcessor.Compand); + Assert.Equal(sampler, resizeProcessor.Options.Sampler); + Assert.Equal(compand, resizeProcessor.Options.Compand); } [Fact] @@ -78,8 +78,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms Assert.Equal(width, resizeProcessor.DestinationWidth); Assert.Equal(height, resizeProcessor.DestinationHeight); - Assert.Equal(sampler, resizeProcessor.Sampler); - Assert.Equal(compand, resizeProcessor.Compand); + Assert.Equal(sampler, resizeProcessor.Options.Sampler); + Assert.Equal(compand, resizeProcessor.Options.Compand); // Ensure options are not altered. Assert.Equal(width, resizeOptions.Size.Width); diff --git a/tests/ImageSharp.Tests/TestFormat.cs b/tests/ImageSharp.Tests/TestFormat.cs index 6c2b97eb6..32da06901 100644 --- a/tests/ImageSharp.Tests/TestFormat.cs +++ b/tests/ImageSharp.Tests/TestFormat.cs @@ -24,7 +24,7 @@ namespace SixLabors.ImageSharp.Tests // We should not change Configuration.Default in individual tests! // Create new configuration instances with new Configuration(TestFormat.GlobalTestFormat) instead! - public static TestFormat GlobalTestFormat { get; } = new TestFormat(); + public static TestFormat GlobalTestFormat { get; } = new(); public TestFormat() { @@ -32,7 +32,7 @@ namespace SixLabors.ImageSharp.Tests this.Decoder = new TestDecoder(this); } - public List DecodeCalls { get; } = new List(); + public List DecodeCalls { get; } = new(); public TestEncoder Encoder { get; } @@ -54,12 +54,12 @@ namespace SixLabors.ImageSharp.Tests return ms; } - public Stream CreateAsyncSamaphoreStream(SemaphoreSlim notifyWaitPositionReachedSemaphore, SemaphoreSlim continueSemaphore, bool seeakable, int size = 1024, int waitAfterPosition = 512) + public Stream CreateAsyncSemaphoreStream(SemaphoreSlim notifyWaitPositionReachedSemaphore, SemaphoreSlim continueSemaphore, bool seeakable, int size = 1024, int waitAfterPosition = 512) { var buffer = new byte[size]; this.header.CopyTo(buffer, 0); var semaphoreStream = new SemaphoreReadMemoryStream(buffer, waitAfterPosition, notifyWaitPositionReachedSemaphore, continueSemaphore); - return seeakable ? (Stream)semaphoreStream : new AsyncStreamWrapper(semaphoreStream, () => false); + return seeakable ? semaphoreStream : new AsyncStreamWrapper(semaphoreStream, () => false); } public void VerifySpecificDecodeCall(byte[] marker, Configuration config) @@ -191,20 +191,14 @@ namespace SixLabors.ImageSharp.Tests return null; } - public TestHeader(TestFormat testFormat) - { - this.testFormat = testFormat; - } + public TestHeader(TestFormat testFormat) => this.testFormat = testFormat; } public class TestDecoder : IImageDecoder, IImageInfoDetector { private readonly TestFormat testFormat; - public TestDecoder(TestFormat testFormat) - { - this.testFormat = testFormat; - } + public TestDecoder(TestFormat testFormat) => this.testFormat = testFormat; public IEnumerable MimeTypes => new[] { this.testFormat.MimeType }; @@ -212,19 +206,15 @@ namespace SixLabors.ImageSharp.Tests public int HeaderSize => this.testFormat.HeaderSize; - public Image Decode(Configuration configuration, Stream stream) + public Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel - => this.DecodeImpl(configuration, stream, default).GetAwaiter().GetResult(); + => this.DecodeImpl(configuration, stream); - public Task> DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel - => this.DecodeImpl(configuration, stream, cancellationToken); - - private async Task> DecodeImpl(Configuration config, Stream stream, CancellationToken cancellationToken) + private Image DecodeImpl(Configuration config, Stream stream) where TPixel : unmanaged, IPixel { var ms = new MemoryStream(); - await stream.CopyToAsync(ms, config.StreamProcessingBufferSize, cancellationToken); + stream.CopyTo(ms, config.StreamProcessingBufferSize); byte[] marker = ms.ToArray().Skip(this.testFormat.header.Length).ToArray(); this.testFormat.DecodeCalls.Add(new DecodeOperation { @@ -239,26 +229,17 @@ namespace SixLabors.ImageSharp.Tests public bool IsSupportedFileFormat(Span header) => this.testFormat.IsSupportedFileFormat(header); - public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream); - - public async Task DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) - => await this.DecodeAsync(configuration, stream, cancellationToken); + public Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken) => this.Decode(configuration, stream, cancellationToken); - public IImageInfo Identify(Configuration configuration, Stream stream) => - this.IdentifyAsync(configuration, stream, default).GetAwaiter().GetResult(); - - public async Task IdentifyAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) - => await this.DecodeImpl(configuration, stream, cancellationToken); + public IImageInfo Identify(Configuration configuration, Stream stream, CancellationToken cancellationToken) => + this.DecodeImpl(configuration, stream); } public class TestEncoder : IImageEncoder { private readonly TestFormat testFormat; - public TestEncoder(TestFormat testFormat) - { - this.testFormat = testFormat; - } + public TestEncoder(TestFormat testFormat) => this.testFormat = testFormat; public IEnumerable MimeTypes => new[] { this.testFormat.MimeType }; @@ -271,16 +252,12 @@ namespace SixLabors.ImageSharp.Tests } public Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel - { - // TODO record this happened so we can verify it. - return Task.CompletedTask; - } + where TPixel : unmanaged, IPixel => Task.CompletedTask; // TODO record this happened so we can verify it. } public struct TestPixelForAgnosticDecode : IPixel { - public PixelOperations CreatePixelOperations() => new PixelOperations(); + public PixelOperations CreatePixelOperations() => new(); public void FromScaledVector4(Vector4 vector) { diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index bce22799d..d36cec630 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -15,6 +15,7 @@ namespace SixLabors.ImageSharp.Tests { public static class Png { + public const string Transparency = "Png/transparency.png"; public const string P1 = "Png/pl.png"; public const string Pd = "Png/pd.png"; public const string Blur = "Png/blur.png"; @@ -65,10 +66,13 @@ namespace SixLabors.ImageSharp.Tests // Filtered test images from http://www.schaik.com/pngsuite/pngsuite_fil_png.html public const string Filter0 = "Png/filter0.png"; - public const string Filter1 = "Png/filter1.png"; - public const string Filter2 = "Png/filter2.png"; - public const string Filter3 = "Png/filter3.png"; - public const string Filter4 = "Png/filter4.png"; + public const string SubFilter3BytesPerPixel = "Png/filter1.png"; + public const string SubFilter4BytesPerPixel = "Png/SubFilter4Bpp.png"; + public const string UpFilter = "Png/filter2.png"; + public const string AverageFilter3BytesPerPixel = "Png/filter3.png"; + public const string AverageFilter4BytesPerPixel = "Png/AverageFilter4Bpp.png"; + public const string PaethFilter3BytesPerPixel = "Png/filter4.png"; + public const string PaethFilter4BytesPerPixel = "Png/PaethFilter4Bpp.png"; // Paletted images also from http://www.schaik.com/pngsuite/pngsuite_fil_png.html public const string PalettedTwoColor = "Png/basn3p01.png"; @@ -126,6 +130,9 @@ namespace SixLabors.ImageSharp.Tests public const string MissingDataChunk = "Png/xdtn0g01.png"; public const string WrongCrcDataChunk = "Png/xcsn0g01.png"; public const string CorruptedChunk = "Png/big-corrupted-chunk.png"; + public const string MissingPaletteChunk1 = "Png/missing_plte.png"; + public const string MissingPaletteChunk2 = "Png/missing_plte_2.png"; + public const string InvalidGammaChunk = "Png/length_gama.png"; // Zlib errors. public const string ZlibOverflow = "Png/zlib-overflow.png"; @@ -150,15 +157,6 @@ namespace SixLabors.ImageSharp.Tests public const string ColorTypeOne = "Png/xc1n0g08.png"; public const string ColorTypeNine = "Png/xc9n2c08.png"; } - - public static readonly string[] All = - { - P1, Pd, Blur, Splash, Cross, - Powerpoint, SplashInterlaced, Interlaced, - Filter0, Filter1, Filter2, Filter3, Filter4, - FilterVar, VimImage1, VimImage2, VersioningImage1, - VersioningImage2, Ratio4x1, Ratio1x4 - }; } public static class Jpeg @@ -191,6 +189,7 @@ namespace SixLabors.ImageSharp.Tests public const string Exif = "Jpg/baseline/exif.jpg"; public const string Floorplan = "Jpg/baseline/Floorplan.jpg"; public const string Calliphora = "Jpg/baseline/Calliphora.jpg"; + public const string Calliphora_EncodedStrings = "Jpg/baseline/Calliphora_encoded_strings.jpg"; public const string Ycck = "Jpg/baseline/ycck.jpg"; public const string Turtle420 = "Jpg/baseline/turtle.jpg"; public const string GammaDalaiLamaGray = "Jpg/baseline/gamma_dalai_lama_gray.jpg"; @@ -215,12 +214,20 @@ namespace SixLabors.ImageSharp.Tests public const string App13WithEmptyIptc = "Jpg/baseline/iptc-psAPP13-wIPTCempty.jpg"; public const string HistogramEqImage = "Jpg/baseline/640px-Unequalized_Hawkes_Bay_NZ.jpg"; public const string ForestBridgeDifferentComponentsQuality = "Jpg/baseline/forest_bridge.jpg"; - public const string ArithmeticCoding = "Jpg/baseline/arithmetic_coding.jpg"; - public const string ArithmeticCodingProgressive = "Jpg/progressive/arithmetic_progressive.jpg"; public const string Lossless = "Jpg/baseline/lossless.jpg"; public const string Winter444_Interleaved = "Jpg/baseline/winter444_interleaved.jpg"; public const string Metadata = "Jpg/baseline/Metadata-test-file.jpg"; public const string ExtendedXmp = "Jpg/baseline/extended-xmp.jpg"; + public const string GrayscaleSampling2x2 = "Jpg/baseline/grayscale_sampling22.jpg"; + + // Jpeg's with arithmetic coding. + public const string ArithmeticCoding01 = "Jpg/baseline/Calliphora_arithmetic.jpg"; + public const string ArithmeticCoding02 = "Jpg/baseline/arithmetic_coding.jpg"; + public const string ArithmeticCodingProgressive01 = "Jpg/progressive/arithmetic_progressive.jpg"; + public const string ArithmeticCodingProgressive02 = "Jpg/progressive/Calliphora-arithmetic-progressive-interleaved.jpg"; + public const string ArithmeticCodingGray = "Jpg/baseline/Calliphora-arithmetic-grayscale.jpg"; + public const string ArithmeticCodingInterleaved = "Jpg/baseline/Calliphora-arithmetic-interleaved.jpg"; + public const string ArithmeticCodingWithRestart = "Jpg/baseline/Calliphora-arithmetic-restart.jpg"; public static readonly string[] All = { @@ -262,6 +269,9 @@ namespace SixLabors.ImageSharp.Tests public const string MalformedUnsupportedComponentCount = "Jpg/issues/issue-1900-malformed-unsupported-255-components.jpg"; public const string MultipleApp01932 = "Jpg/issues/issue-1932-app0-resolution.jpg"; public const string InvalidIptcTag = "Jpg/issues/Issue1942InvalidIptcTag.jpg"; + public const string Issue2057App1Parsing = "Jpg/issues/Issue2057-App1Parsing.jpg"; + public const string ExifNullArrayTag = "Jpg/issues/issue-2056-exif-null-array.jpg"; + public const string ValidExifArgumentNullExceptionOnEncode = "Jpg/issues/Issue2087-exif-null-reference-on-encode.jpg"; public static class Fuzz { @@ -290,6 +300,7 @@ namespace SixLabors.ImageSharp.Tests public const string AccessViolationException922 = "Jpg/issues/fuzz/Issue922-AccessViolationException.jpg"; public const string IndexOutOfRangeException1693A = "Jpg/issues/fuzz/Issue1693-IndexOutOfRangeException-A.jpg"; public const string IndexOutOfRangeException1693B = "Jpg/issues/fuzz/Issue1693-IndexOutOfRangeException-B.jpg"; + public const string NullReferenceException2085 = "Jpg/issues/fuzz/Issue2085-NullReferenceException.jpg"; } } @@ -368,6 +379,7 @@ namespace SixLabors.ImageSharp.Tests public const string Rgb24jpeg = "Bmp/rgb24jpeg.bmp"; public const string Rgb24png = "Bmp/rgb24png.bmp"; public const string Rgba32v4 = "Bmp/rgba32v4.bmp"; + public const string IccProfile = "Bmp/BMP_v5_with_ICC_2.bmp"; // Bitmap images with compression type BITFIELDS. public const string Rgb32bfdef = "Bmp/rgb32bfdef.bmp"; @@ -445,9 +457,13 @@ namespace SixLabors.ImageSharp.Tests public const string BadAppExtLength = "Gif/issues/issue405_badappextlength252.gif"; public const string BadAppExtLength_2 = "Gif/issues/issue405_badappextlength252-2.gif"; public const string BadDescriptorWidth = "Gif/issues/issue403_baddescriptorwidth.gif"; + public const string DeferredClearCode = "Gif/issues/bugzilla-55918.gif"; public const string Issue1505 = "Gif/issues/issue1505_argumentoutofrange.png"; public const string Issue1530 = "Gif/issues/issue1530.gif"; public const string InvalidColorIndex = "Gif/issues/issue1668_invalidcolorindex.gif"; + public const string Issue1962NoColorTable = "Gif/issues/issue1962_tiniest_gif_1st.gif"; + public const string Issue2012EmptyXmp = "Gif/issues/issue2012_Stronghold-Crusader-Extreme-Cover.gif"; + public const string Issue2012BadMinCode = "Gif/issues/issue2012_drona1.gif"; } public static readonly string[] All = { Rings, Giphy, Cheers, Trans, Kumin, Leo, Ratio4x1, Ratio1x4 }; @@ -629,6 +645,7 @@ namespace SixLabors.ImageSharp.Tests { public const string Earth = "Webp/earth_lossy.webp"; public const string WithExif = "Webp/exif_lossy.webp"; + public const string WithExifNotEnoughData = "Webp/exif_lossy_not_enough_data.webp"; public const string WithIccp = "Webp/lossy_with_iccp.webp"; public const string WithXmp = "Webp/xmp_lossy.webp"; public const string BikeSmall = "Webp/bike_lossless_small.webp"; @@ -762,6 +779,7 @@ namespace SixLabors.ImageSharp.Tests public const string GrayscaleDeflateMultistrip = "Tiff/grayscale_deflate_multistrip.tiff"; public const string GrayscaleUncompressed = "Tiff/grayscale_uncompressed.tiff"; + public const string GrayscaleJpegCompressed = "Tiff/JpegCompressedGray.tiff"; public const string PaletteDeflateMultistrip = "Tiff/palette_grayscale_deflate_multistrip.tiff"; public const string PaletteUncompressed = "Tiff/palette_uncompressed.tiff"; public const string RgbDeflate = "Tiff/rgb_deflate.tiff"; @@ -826,6 +844,9 @@ namespace SixLabors.ImageSharp.Tests public const string Flower2BitPalette = "Tiff/flower-palette-02.tiff"; public const string Flower4BitPalette = "Tiff/flower-palette-04.tiff"; public const string Flower4BitPaletteGray = "Tiff/flower-minisblack-04.tiff"; + public const string FLowerRgb3Bit = "Tiff/flower-rgb-3bit.tiff"; + public const string FLowerRgb5Bit = "Tiff/flower-rgb-5bit.tiff"; + public const string FLowerRgb6Bit = "Tiff/flower-rgb-6bit.tiff"; public const string Flower6BitGray = "Tiff/flower-minisblack-06.tiff"; public const string Flower8BitGray = "Tiff/flower-minisblack-08.tiff"; public const string Flower10BitGray = "Tiff/flower-minisblack-10.tiff"; @@ -850,6 +871,51 @@ namespace SixLabors.ImageSharp.Tests public const string Flower32BitGrayPredictorBigEndian = "Tiff/flower-minisblack-32_msb_deflate_predictor.tiff"; public const string Flower32BitGrayPredictorLittleEndian = "Tiff/flower-minisblack-32_lsb_deflate_predictor.tiff"; + // Images with alpha channel. + public const string Rgba2BitUnassociatedAlpha = "Tiff/RgbaUnassociatedAlpha2bit.tiff"; + public const string Rgba3BitUnassociatedAlpha = "Tiff/RgbaUnassociatedAlpha3bit.tiff"; + public const string Rgba3BitAssociatedAlpha = "Tiff/RgbaAssociatedAlpha3bit.tiff"; + public const string Rgba4BitUnassociatedAlpha = "Tiff/RgbaUnassociatedAlpha4bit.tiff"; + public const string Rgba4BitAassociatedAlpha = "Tiff/RgbaAssociatedAlpha4bit.tiff"; + public const string Rgba5BitUnassociatedAlpha = "Tiff/RgbaUnassociatedAlpha5bit.tiff"; + public const string Rgba5BitAssociatedAlpha = "Tiff/RgbaAssociatedAlpha5bit.tiff"; + public const string Rgba6BitUnassociatedAlpha = "Tiff/RgbaUnassociatedAlpha6bit.tiff"; + public const string Rgba6BitAssociatedAlpha = "Tiff/RgbaAssociatedAlpha6bit.tiff"; + public const string Rgba8BitUnassociatedAlpha = "Tiff/RgbaUnassociatedAlpha8bit.tiff"; + public const string Rgba8BitAssociatedAlpha = "Tiff/RgbaAlpha8bit.tiff"; + public const string Rgba8BitUnassociatedAlphaWithPredictor = "Tiff/RgbaUnassociatedAlphaPredictor8bit.tiff"; + public const string Rgba8BitPlanarUnassociatedAlpha = "Tiff/RgbaUnassociatedAlphaPlanar8bit.tiff"; + public const string Rgba10BitUnassociatedAlphaBigEndian = "Tiff/RgbaUnassociatedAlpha10bit_msb.tiff"; + public const string Rgba10BitUnassociatedAlphaLittleEndian = "Tiff/RgbaUnassociatedAlpha10bit_lsb.tiff"; + public const string Rgba10BitAssociatedAlphaBigEndian = "Tiff/RgbaAssociatedAlpha10bit_msb.tiff"; + public const string Rgba10BitAssociatedAlphaLittleEndian = "Tiff/RgbaAssociatedAlpha10bit_lsb.tiff"; + public const string Rgba12BitUnassociatedAlphaBigEndian = "Tiff/RgbaUnassociatedAlpha12bit_msb.tiff"; + public const string Rgba12BitUnassociatedAlphaLittleEndian = "Tiff/RgbaUnassociatedAlpha12bit_lsb.tiff"; + public const string Rgba12BitAssociatedAlphaBigEndian = "Tiff/RgbaAssociatedAlpha12bit_msb.tiff"; + public const string Rgba12BitAssociatedAlphaLittleEndian = "Tiff/RgbaAssociatedAlpha12bit_lsb.tiff"; + public const string Rgba14BitUnassociatedAlphaBigEndian = "Tiff/RgbaUnassociatedAlpha14bit_msb.tiff"; + public const string Rgba14BitUnassociatedAlphaLittleEndian = "Tiff/RgbaUnassociatedAlpha14bit_lsb.tiff"; + public const string Rgba14BitAssociatedAlphaBigEndian = "Tiff/RgbaAssociatedAlpha14bit_msb.tiff"; + public const string Rgba14BitAssociatedAlphaLittleEndian = "Tiff/RgbaAssociatedAlpha14bit_lsb.tiff"; + public const string Rgba16BitUnassociatedAlphaBigEndian = "Tiff/RgbaUnassociatedAlpha16bit_msb.tiff"; + public const string Rgba16BitUnassociatedAlphaLittleEndian = "Tiff/RgbaUnassociatedAlpha16bit_lsb.tiff"; + public const string Rgba16BitAssociatedAlphaBigEndian = "Tiff/RgbaAssociatedAlpha16bit_msb.tiff"; + public const string Rgba16BitAssociatedAlphaLittleEndian = "Tiff/RgbaAssociatedAlpha16bit_lsb.tiff"; + public const string Rgba16BitUnassociatedAlphaBigEndianWithPredictor = "Tiff/RgbaUnassociatedAlphaPredictor16bit_msb.tiff"; + public const string Rgba16BitUnassociatedAlphaLittleEndianWithPredictor = "Tiff/RgbaUnassociatedAlphaPredictor16bit_lsb.tiff"; + public const string Rgba16BitPlanarUnassociatedAlphaBigEndian = "Tiff/RgbaUnassociatedAlphaPlanar16bit_msb.tiff"; + public const string Rgba16BitPlanarUnassociatedAlphaLittleEndian = "Tiff/RgbaUnassociatedAlphaPlanar16bit_lsb.tiff"; + public const string Rgba24BitUnassociatedAlphaBigEndian = "Tiff/RgbaUnassociatedAlpha24bit_msb.tiff"; + public const string Rgba24BitUnassociatedAlphaLittleEndian = "Tiff/RgbaUnassociatedAlpha24bit_lsb.tiff"; + public const string Rgba24BitPlanarUnassociatedAlphaBigEndian = "Tiff/RgbaUnassociatedAlphaPlanar24bit_msb.tiff"; + public const string Rgba24BitPlanarUnassociatedAlphaLittleEndian = "Tiff/RgbaUnassociatedAlphaPlanar24bit_lsb.tiff"; + public const string Rgba32BitUnassociatedAlphaBigEndian = "Tiff/RgbaUnassociatedAlpha32bit_msb.tiff"; + public const string Rgba32BitUnassociatedAlphaLittleEndian = "Tiff/RgbaUnassociatedAlpha32bit_lsb.tiff"; + public const string Rgba32BitUnassociatedAlphaBigEndianWithPredictor = "Tiff/RgbaUnassociatedAlphaPredictor32bit_msb.tiff"; + public const string Rgba32BitUnassociatedAlphaLittleEndianWithPredictor = "Tiff/RgbaUnassociatedAlphaPredictor32bit_lsb.tiff"; + public const string Rgba32BitPlanarUnassociatedAlphaBigEndian = "Tiff/RgbaUnassociatedAlphaPlanar32bit_msb.tiff"; + public const string Rgba32BitPlanarUnassociatedAlphaLittleEndian = "Tiff/RgbaUnassociatedAlphaPlanar32bit_lsb.tiff"; + public const string Issues1716Rgb161616BitLittleEndian = "Tiff/Issues/Issue1716.tiff"; public const string Issues1891 = "Tiff/Issues/Issue1891.tiff"; @@ -863,6 +929,7 @@ namespace SixLabors.ImageSharp.Tests public const string MultiframeDeflateWithPreview = "Tiff/multipage_deflate_withPreview.tiff"; public const string MultiframeDifferentSize = "Tiff/multipage_differentSize.tiff"; public const string MultiframeDifferentVariants = "Tiff/multipage_differentVariants.tiff"; + public const string MultiFrameMipMap = "Tiff/SKC1H3.tiff"; public const string LittleEndianByteOrder = "Tiff/little_endian.tiff"; diff --git a/tests/ImageSharp.Tests/TestUtilities/FeatureTesting/FeatureTestRunner.cs b/tests/ImageSharp.Tests/TestUtilities/FeatureTesting/FeatureTestRunner.cs index 0d2f3fcef..fc0374bbd 100644 --- a/tests/ImageSharp.Tests/TestUtilities/FeatureTesting/FeatureTestRunner.cs +++ b/tests/ImageSharp.Tests/TestUtilities/FeatureTesting/FeatureTestRunner.cs @@ -356,10 +356,6 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities var key = (HwIntrinsics)Enum.Parse(typeof(HwIntrinsics), intrinsic); switch (intrinsic) { - case nameof(HwIntrinsics.DisableSIMD): - features.Add(key, "FeatureSIMD"); - break; - case nameof(HwIntrinsics.AllowAll): // Not a COMPlus value. We filter in calling method. @@ -390,23 +386,22 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities { // Use flags so we can pass multiple values without using params. // Don't base on 0 or use inverse for All as that doesn't translate to string values. - DisableSIMD = 1 << 0, - DisableHWIntrinsic = 1 << 1, - DisableSSE = 1 << 2, - DisableSSE2 = 1 << 3, - DisableAES = 1 << 4, - DisablePCLMULQDQ = 1 << 5, - DisableSSE3 = 1 << 6, - DisableSSSE3 = 1 << 7, - DisableSSE41 = 1 << 8, - DisableSSE42 = 1 << 9, - DisablePOPCNT = 1 << 10, - DisableAVX = 1 << 11, - DisableFMA = 1 << 12, - DisableAVX2 = 1 << 13, - DisableBMI1 = 1 << 14, - DisableBMI2 = 1 << 15, - DisableLZCNT = 1 << 16, - AllowAll = 1 << 17 + DisableHWIntrinsic = 1 << 0, + DisableSSE = 1 << 1, + DisableSSE2 = 1 << 2, + DisableAES = 1 << 3, + DisablePCLMULQDQ = 1 << 4, + DisableSSE3 = 1 << 5, + DisableSSSE3 = 1 << 6, + DisableSSE41 = 1 << 7, + DisableSSE42 = 1 << 8, + DisablePOPCNT = 1 << 9, + DisableAVX = 1 << 10, + DisableFMA = 1 << 11, + DisableAVX2 = 1 << 12, + DisableBMI1 = 1 << 13, + DisableBMI2 = 1 << 14, + DisableLZCNT = 1 << 15, + AllowAll = 1 << 16 } } diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/Exceptions/ImageDifferenceIsOverThresholdException.cs b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/Exceptions/ImageDifferenceIsOverThresholdException.cs index e1cbf12ac..220b78e48 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/Exceptions/ImageDifferenceIsOverThresholdException.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/Exceptions/ImageDifferenceIsOverThresholdException.cs @@ -24,7 +24,7 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison sb.Append(Environment.NewLine); - // TODO: We should add OSX. + // TODO: We should add macOS. sb.AppendFormat("Test Environment OS : {0}", TestEnvironment.IsWindows ? "Windows" : "Linux"); sb.Append(Environment.NewLine); diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparingUtils.cs b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparingUtils.cs index 1801d6b59..fa2584674 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparingUtils.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparingUtils.cs @@ -26,7 +26,7 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison } var testFile = TestFile.Create(path); - Image magickImage = DecodeWithMagick(new FileInfo(testFile.FullPath)); + using Image magickImage = DecodeWithMagick(new FileInfo(testFile.FullPath)); if (useExactComparer) { ImageComparer.Exact.VerifySimilarity(magickImage, image); diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs index e2e7d73bc..63c5ce31a 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.IO; using System.Reflection; using System.Threading.Tasks; +using SixLabors.ImageSharp.Diagnostics; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -158,8 +159,13 @@ namespace SixLabors.ImageSharp.Tests return this.LoadImage(decoder); } - var key = new Key(this.PixelType, this.FilePath, decoder); + // do not cache so we can track allocation correctly when validating memory + if (MemoryAllocatorValidator.MonitoringAllocations) + { + return this.LoadImage(decoder); + } + var key = new Key(this.PixelType, this.FilePath, decoder); Image cachedImage = Cache.GetOrAdd(key, _ => this.LoadImage(decoder)); return cachedImage.Clone(this.Configuration); diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs index f0834dc00..8160118aa 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; using System.Threading; -using System.Threading.Tasks; using ImageMagick; using ImageMagick.Formats; using SixLabors.ImageSharp.Formats; @@ -59,11 +58,7 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs } } - public Task> DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, ImageSharp.PixelFormats.IPixel - => Task.FromResult(this.Decode(configuration, stream)); - - public Image Decode(Configuration configuration, Stream stream) + public Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, ImageSharp.PixelFormats.IPixel { var bmpReadDefines = new BmpReadDefines @@ -84,13 +79,13 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs MemoryGroup framePixels = frame.PixelBuffer.FastMemoryGroup; using IUnsafePixelCollection pixels = magicFrame.GetPixelsUnsafe(); - if (magicFrame.Depth == 8 || magicFrame.Depth == 6 || magicFrame.Depth == 4 || magicFrame.Depth == 2 || magicFrame.Depth == 1 || magicFrame.Depth == 10 || magicFrame.Depth == 12) + if (magicFrame.Depth is 12 or 10 or 8 or 6 or 5 or 4 or 3 or 2 or 1) { byte[] data = pixels.ToByteArray(PixelMapping.RGBA); FromRgba32Bytes(configuration, data, framePixels); } - else if (magicFrame.Depth == 16 || magicFrame.Depth == 14) + else if (magicFrame.Depth is 16 or 14) { ushort[] data = pixels.ToShortArray(PixelMapping.RGBA); Span bytes = MemoryMarshal.Cast(data.AsSpan()); @@ -105,9 +100,6 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs return new Image(configuration, new ImageMetadata(), framesList); } - public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream); - - public async Task DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) - => await this.DecodeAsync(configuration, stream, cancellationToken); + public Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken) => this.Decode(configuration, stream, cancellationToken); } } diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs index 1eb1328ef..110814480 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingReferenceDecoder.cs @@ -3,8 +3,6 @@ using System.IO; using System.Threading; -using System.Threading.Tasks; -using SixLabors.ImageSharp.ColorSpaces.Conversion; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; @@ -15,11 +13,7 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs { public static SystemDrawingReferenceDecoder Instance { get; } = new SystemDrawingReferenceDecoder(); - public Task> DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel - => Task.FromResult(this.Decode(configuration, stream)); - - public Image Decode(Configuration configuration, Stream stream) + public Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { using (var sourceBitmap = new System.Drawing.Bitmap(stream)) @@ -49,10 +43,7 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs } } - public Task IdentifyAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) - => Task.FromResult(this.Identify(configuration, stream)); - - public IImageInfo Identify(Configuration configuration, Stream stream) + public IImageInfo Identify(Configuration configuration, Stream stream, CancellationToken cancellationToken) { using (var sourceBitmap = new System.Drawing.Bitmap(stream)) { @@ -61,9 +52,6 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs } } - public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream); - - public async Task DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) - => await this.DecodeAsync(configuration, stream, cancellationToken); + public Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken) => this.Decode(configuration, stream, cancellationToken); } } diff --git a/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.cs b/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.cs index 3ccaf2ba3..df584173e 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.cs @@ -110,7 +110,7 @@ namespace SixLabors.ImageSharp.Tests internal static bool IsLinux => RuntimeInformation.IsOSPlatform(OSPlatform.Linux); - internal static bool IsOSX => RuntimeInformation.IsOSPlatform(OSPlatform.OSX); + internal static bool IsMacOS => RuntimeInformation.IsOSPlatform(OSPlatform.OSX); internal static bool IsMono => Type.GetType("Mono.Runtime") != null; // https://stackoverflow.com/a/721194 diff --git a/tests/ImageSharp.Tests/TestUtilities/TestPixel.cs b/tests/ImageSharp.Tests/TestUtilities/TestPixel.cs index 7265e29c3..104c54cee 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestPixel.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestPixel.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Numerics; using SixLabors.ImageSharp.PixelFormats; using Xunit.Abstractions; @@ -16,6 +17,11 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities public TestPixel(float red, float green, float blue, float alpha) { + Guard.MustBeBetweenOrEqualTo(red, 0F, 1F, nameof(red)); + Guard.MustBeBetweenOrEqualTo(green, 0F, 1F, nameof(green)); + Guard.MustBeBetweenOrEqualTo(blue, 0F, 1F, nameof(blue)); + Guard.MustBeBetweenOrEqualTo(alpha, 0F, 1F, nameof(alpha)); + this.Red = red; this.Green = green; this.Blue = blue; @@ -33,14 +39,11 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities public TPixel AsPixel() { var pix = default(TPixel); - pix.FromVector4(new System.Numerics.Vector4(this.Red, this.Green, this.Blue, this.Alpha)); + pix.FromScaledVector4(new Vector4(this.Red, this.Green, this.Blue, this.Alpha)); return pix; } - internal Span AsSpan() - { - return new Span(new[] { this.AsPixel() }); - } + internal Span AsSpan() => new(new[] { this.AsPixel() }); public void Deserialize(IXunitSerializationInfo info) { @@ -58,9 +61,6 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities info.AddValue("alpha", this.Alpha); } - public override string ToString() - { - return $"{typeof(TPixel).Name}{this.AsPixel().ToString()}"; - } + public override string ToString() => $"{typeof(TPixel).Name}{this.AsPixel()}"; } } diff --git a/tests/ImageSharp.Tests/TestUtilities/Tests/FeatureTestRunnerTests.cs b/tests/ImageSharp.Tests/TestUtilities/Tests/FeatureTestRunnerTests.cs index a2f36c85a..6dd59a750 100644 --- a/tests/ImageSharp.Tests/TestUtilities/Tests/FeatureTestRunnerTests.cs +++ b/tests/ImageSharp.Tests/TestUtilities/Tests/FeatureTestRunnerTests.cs @@ -16,10 +16,10 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.Tests public class FeatureTestRunnerTests { public static TheoryData Intrinsics => - new TheoryData + new() { { HwIntrinsics.DisableAES | HwIntrinsics.AllowAll, new string[] { "EnableAES", "AllowAll" } }, - { HwIntrinsics.DisableSIMD | HwIntrinsics.DisableHWIntrinsic, new string[] { "FeatureSIMD", "EnableHWIntrinsic" } }, + { HwIntrinsics.DisableHWIntrinsic, new string[] { "EnableHWIntrinsic" } }, { HwIntrinsics.DisableSSE42 | HwIntrinsics.DisableAVX, new string[] { "EnableSSE42", "EnableAVX" } } }; @@ -55,14 +55,6 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.Tests HwIntrinsics.AllowAll); } - [Fact] - public void CanLimitHwIntrinsicSIMDFeatures() - { - FeatureTestRunner.RunWithHwIntrinsicsFeature( - () => Assert.False(Vector.IsHardwareAccelerated), - HwIntrinsics.DisableSIMD); - } - #if SUPPORTS_RUNTIME_INTRINSICS [Fact] public void CanLimitHwIntrinsicBaseFeatures() @@ -101,9 +93,6 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.Tests switch ((HwIntrinsics)Enum.Parse(typeof(HwIntrinsics), intrinsic)) { - case HwIntrinsics.DisableSIMD: - Assert.False(Vector.IsHardwareAccelerated); - break; #if SUPPORTS_RUNTIME_INTRINSICS case HwIntrinsics.DisableHWIntrinsic: Assert.False(Sse.IsSupported); @@ -206,9 +195,6 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.Tests switch ((HwIntrinsics)Enum.Parse(typeof(HwIntrinsics), intrinsic)) { - case HwIntrinsics.DisableSIMD: - Assert.False(Vector.IsHardwareAccelerated, nameof(Vector.IsHardwareAccelerated)); - break; #if SUPPORTS_RUNTIME_INTRINSICS case HwIntrinsics.DisableHWIntrinsic: Assert.False(Sse.IsSupported); diff --git a/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs b/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs index 129d17f4d..4542257c6 100644 --- a/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs +++ b/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Concurrent; using System.IO; using System.Threading; -using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Memory; @@ -350,9 +349,6 @@ namespace SixLabors.ImageSharp.Tests private static readonly ConcurrentDictionary InvocationCounts = new ConcurrentDictionary(); - private static readonly ConcurrentDictionary InvocationCountsAsync = - new ConcurrentDictionary(); - private static readonly object Monitor = new object(); private string callerName; @@ -365,35 +361,22 @@ namespace SixLabors.ImageSharp.Tests } } - public Image Decode(Configuration configuration, Stream stream) + public Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { InvocationCounts[this.callerName]++; return new Image(42, 42); } - public Task> DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel - { - InvocationCountsAsync[this.callerName]++; - return Task.FromResult(new Image(42, 42)); - } - internal static int GetInvocationCount(string callerName) => InvocationCounts[callerName]; - internal static int GetInvocationCountAsync(string callerName) => InvocationCountsAsync[callerName]; - internal void InitCaller(string name) { this.callerName = name; InvocationCounts[name] = 0; - InvocationCountsAsync[name] = 0; } - public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream); - - public async Task DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) - => await this.DecodeAsync(configuration, stream, cancellationToken); + public Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken) => this.Decode(configuration, stream, cancellationToken); } private class TestDecoderWithParameters : IImageDecoder @@ -401,9 +384,6 @@ namespace SixLabors.ImageSharp.Tests private static readonly ConcurrentDictionary InvocationCounts = new ConcurrentDictionary(); - private static readonly ConcurrentDictionary InvocationCountsAsync = - new ConcurrentDictionary(); - private static readonly object Monitor = new object(); private string callerName; @@ -420,35 +400,22 @@ namespace SixLabors.ImageSharp.Tests } } - public Image Decode(Configuration configuration, Stream stream) + public Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { InvocationCounts[this.callerName]++; return new Image(42, 42); } - public Task> DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel - { - InvocationCountsAsync[this.callerName]++; - return Task.FromResult(new Image(42, 42)); - } - internal static int GetInvocationCount(string callerName) => InvocationCounts[callerName]; - internal static int GetInvocationCountAsync(string callerName) => InvocationCountsAsync[callerName]; - internal void InitCaller(string name) { this.callerName = name; InvocationCounts[name] = 0; - InvocationCountsAsync[name] = 0; } - public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream); - - public async Task DecodeAsync(Configuration configuration, Stream stream, CancellationToken cancellationToken) - => await this.DecodeAsync(configuration, stream, cancellationToken); + public Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken) => this.Decode(configuration, stream, cancellationToken); } } } diff --git a/tests/ImageSharp.Tests/ValidateDisposedMemoryAllocationsAttribute.cs b/tests/ImageSharp.Tests/ValidateDisposedMemoryAllocationsAttribute.cs new file mode 100644 index 000000000..65ed990dd --- /dev/null +++ b/tests/ImageSharp.Tests/ValidateDisposedMemoryAllocationsAttribute.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Diagnostics; +using System.Reflection; +using Xunit.Sdk; + +namespace SixLabors.ImageSharp.Tests +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public class ValidateDisposedMemoryAllocationsAttribute : BeforeAfterTestAttribute + { + private readonly int expected = 0; + + public ValidateDisposedMemoryAllocationsAttribute() + : this(0) + { + } + + public ValidateDisposedMemoryAllocationsAttribute(int expected) + => this.expected = expected; + + public override void Before(MethodInfo methodUnderTest) + => MemoryAllocatorValidator.MonitorAllocations(); + + public override void After(MethodInfo methodUnderTest) + { + MemoryAllocatorValidator.ValidateAllocations(this.expected); + MemoryAllocatorValidator.StopMonitoringAllocations(); + } + } +} diff --git a/tests/Images/External/README.md b/tests/Images/External/README.md index 936b15e6a..9e9fcf5e1 100644 --- a/tests/Images/External/README.md +++ b/tests/Images/External/README.md @@ -5,4 +5,4 @@ Contains images to validate against in ImageSharp tests. In most cases the file Various utilities to help dealing with images. - `optipng.exe`: [lossless PNG compressor](http://optipng.sourceforge.net/), to keep the `ReferenceImages` folder as small as possible - `optimize-all.cmd`: Runs lossless optimizer for reference PNG-s. Currently it has to be manually edited to add new test class directories. -- [`dump-jpeg-coeffs.exe`](https://github.com/SixLabors/Imagesharp/blob/master/tests/Images/External/tools/jpeg/README.md) +- [`dump-jpeg-coeffs.exe`](https://github.com/SixLabors/Imagesharp/blob/main/tests/Images/External/tools/jpeg/README.md) diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue1962_Rgba32_issue1962_tiniest_gif_1st.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue1962_Rgba32_issue1962_tiniest_gif_1st.png new file mode 100644 index 000000000..24f5e9c0c --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue1962_Rgba32_issue1962_tiniest_gif_1st.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4f8c6d416f09671777934e57bc67fb52ccc97145dc6f1869e628d9ffd7d8f6e7 +size 119 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2012EmptyXmp_Rgba32_issue2012_Stronghold-Crusader-Extreme-Cover.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2012EmptyXmp_Rgba32_issue2012_Stronghold-Crusader-Extreme-Cover.png new file mode 100644 index 000000000..c646eb860 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Issue2012EmptyXmp_Rgba32_issue2012_Stronghold-Crusader-Extreme-Cover.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ba8295d8a4b087d6c19fbad7e97cef7b5ce1a69b9c4c4f79cee6bc77e41f236 +size 62778 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/IssueDeferredClearCode_Rgba32_bugzilla-55918.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/IssueDeferredClearCode_Rgba32_bugzilla-55918.png new file mode 100644 index 000000000..b5769c2c4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/IssueDeferredClearCode_Rgba32_bugzilla-55918.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b33733518b855b25c5e9a1b2f5c93cacf0699a40a459dde795b0ed91a978909 +size 12776 diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_grayscale_sampling22.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_grayscale_sampling22.png new file mode 100644 index 000000000..b2c3effdd --- /dev/null +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_grayscale_sampling22.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad22f28d20ea0ceda983a138b3bf9503ed836d779ed75a313f668329c910665e +size 168405 diff --git a/tests/Images/External/ReferenceOutput/ResizeTests/ResizeWithBoxPadMode_CalliphoraPartial.png b/tests/Images/External/ReferenceOutput/ResizeTests/ResizeWithBoxPadMode_CalliphoraPartial.png index 35d633c86..804c5dcf9 100644 --- a/tests/Images/External/ReferenceOutput/ResizeTests/ResizeWithBoxPadMode_CalliphoraPartial.png +++ b/tests/Images/External/ReferenceOutput/ResizeTests/ResizeWithBoxPadMode_CalliphoraPartial.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1642520a9d4491f55af5a83280423929529e3d528637538d148b9dc2ecdd6d2d -size 365839 +oid sha256:77086032bab11b91c68bc1686179063edbadc9d453574e4f087b2bbd677b4c8e +size 402367 diff --git a/tests/Images/External/ReferenceOutput/ResizeTests/ResizeWithPadMode_CalliphoraPartial.png b/tests/Images/External/ReferenceOutput/ResizeTests/ResizeWithPadMode_CalliphoraPartial.png index 690c2ad46..bfa048f82 100644 --- a/tests/Images/External/ReferenceOutput/ResizeTests/ResizeWithPadMode_CalliphoraPartial.png +++ b/tests/Images/External/ReferenceOutput/ResizeTests/ResizeWithPadMode_CalliphoraPartial.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:060826324dcd4baa1df1b075707a9bd9527cf87c1d156e3beb3d8abb499bdcd5 -size 361829 +oid sha256:5c0653aa2b726574fbea4cc308c269ff5e534d38bb48c0e77470c11042a395fd +size 400267 diff --git a/tests/Images/Input/Bmp/BMP_v5_with_ICC_2.bmp b/tests/Images/Input/Bmp/BMP_v5_with_ICC_2.bmp new file mode 100644 index 000000000..d6328d429 --- /dev/null +++ b/tests/Images/Input/Bmp/BMP_v5_with_ICC_2.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b5b483e9a9d3f3ebdeada2eff70800002c27c046bf971206af0ecc73fa1416e6 +size 27782 diff --git a/tests/Images/Input/Gif/issues/bugzilla-55918.gif b/tests/Images/Input/Gif/issues/bugzilla-55918.gif new file mode 100644 index 000000000..929ea67c3 --- /dev/null +++ b/tests/Images/Input/Gif/issues/bugzilla-55918.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d11148669a093c2e39be62bc3482c5863362d28c03c7f26c5a2386d5de28c339 +size 14551 diff --git a/tests/Images/Input/Gif/issues/issue1962_tiniest_gif_1st.gif b/tests/Images/Input/Gif/issues/issue1962_tiniest_gif_1st.gif new file mode 100644 index 000000000..a5bc432d8 --- /dev/null +++ b/tests/Images/Input/Gif/issues/issue1962_tiniest_gif_1st.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3b7b8a4b411ddf8db9bacc2f3aabf406f8e4c0c087829b336ca331c40adfdff1 +size 26 diff --git a/tests/Images/Input/Gif/issues/issue2012_Stronghold-Crusader-Extreme-Cover.gif b/tests/Images/Input/Gif/issues/issue2012_Stronghold-Crusader-Extreme-Cover.gif new file mode 100644 index 000000000..90f0e0f1c --- /dev/null +++ b/tests/Images/Input/Gif/issues/issue2012_Stronghold-Crusader-Extreme-Cover.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:97f8fdbabfbd9663bf9940dc33f81edf330b62789d1aa573ae85a520903723e5 +size 77498 diff --git a/tests/Images/Input/Gif/issues/issue2012_drona1.gif b/tests/Images/Input/Gif/issues/issue2012_drona1.gif new file mode 100644 index 000000000..803d68487 --- /dev/null +++ b/tests/Images/Input/Gif/issues/issue2012_drona1.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cbb23b2a19e314969c6da99374ae133d834d76c3f0ab9df4a7edc9334bb065e6 +size 10508 diff --git a/tests/Images/Input/Jpg/baseline/Calliphora-arithmetic-grayscale.jpg b/tests/Images/Input/Jpg/baseline/Calliphora-arithmetic-grayscale.jpg new file mode 100644 index 000000000..59256be59 --- /dev/null +++ b/tests/Images/Input/Jpg/baseline/Calliphora-arithmetic-grayscale.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c5800857969f25773606ecb63154a7fee60b831f13932dc845e50276bac11b16 +size 177311 diff --git a/tests/Images/Input/Jpg/baseline/Calliphora-arithmetic-interleaved.jpg b/tests/Images/Input/Jpg/baseline/Calliphora-arithmetic-interleaved.jpg new file mode 100644 index 000000000..9dc93473d --- /dev/null +++ b/tests/Images/Input/Jpg/baseline/Calliphora-arithmetic-interleaved.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:09bb5d75c3ca9d92d6e6489611f1d9b9815aaec70a16027f4ca17e371aa69e6e +size 234032 diff --git a/tests/Images/Input/Jpg/baseline/Calliphora-arithmetic-restart.jpg b/tests/Images/Input/Jpg/baseline/Calliphora-arithmetic-restart.jpg new file mode 100644 index 000000000..5ba56de1a --- /dev/null +++ b/tests/Images/Input/Jpg/baseline/Calliphora-arithmetic-restart.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad547d0de50d1d623a49dc7794838c6d35372ea3fe4bda06365ff8b42daf65bf +size 234225 diff --git a/tests/Images/Input/Jpg/baseline/Calliphora_arithmetic.jpg b/tests/Images/Input/Jpg/baseline/Calliphora_arithmetic.jpg new file mode 100644 index 000000000..9dc93473d --- /dev/null +++ b/tests/Images/Input/Jpg/baseline/Calliphora_arithmetic.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:09bb5d75c3ca9d92d6e6489611f1d9b9815aaec70a16027f4ca17e371aa69e6e +size 234032 diff --git a/tests/Images/Input/Jpg/baseline/Calliphora_encoded_strings.jpg b/tests/Images/Input/Jpg/baseline/Calliphora_encoded_strings.jpg new file mode 100644 index 000000000..b652ed2e5 --- /dev/null +++ b/tests/Images/Input/Jpg/baseline/Calliphora_encoded_strings.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:59f76d2935a619d128a63d6bfcd5ce9feec492a7f5175327e47554c90b4ec242 +size 258081 diff --git a/tests/Images/Input/Jpg/baseline/grayscale_sampling22.jpg b/tests/Images/Input/Jpg/baseline/grayscale_sampling22.jpg new file mode 100644 index 000000000..b861c68ab --- /dev/null +++ b/tests/Images/Input/Jpg/baseline/grayscale_sampling22.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a5cc8572082a54944d48b3e4f49e6c441871f6eb2b616fbbbfb025f20e0aeff5 +size 45066 diff --git a/tests/Images/Input/Jpg/issues/Issue2057-App1Parsing.jpg b/tests/Images/Input/Jpg/issues/Issue2057-App1Parsing.jpg new file mode 100644 index 000000000..d1ffe4ac9 --- /dev/null +++ b/tests/Images/Input/Jpg/issues/Issue2057-App1Parsing.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7048dee15946bf981e5b0d2481ffcb8a64684fddca07172275b13a05f01b6b63 +size 1631109 diff --git a/tests/Images/Input/Jpg/issues/Issue2087-exif-null-reference-on-encode.jpg b/tests/Images/Input/Jpg/issues/Issue2087-exif-null-reference-on-encode.jpg new file mode 100644 index 000000000..e95ef7a73 --- /dev/null +++ b/tests/Images/Input/Jpg/issues/Issue2087-exif-null-reference-on-encode.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4d41a41180a3371d0c4a724b40a4c86f6f975dab6be9da96964a484818770394 +size 30715 diff --git a/tests/Images/Input/Jpg/issues/fuzz/Issue2085-NullReferenceException.jpg b/tests/Images/Input/Jpg/issues/fuzz/Issue2085-NullReferenceException.jpg new file mode 100644 index 000000000..8a680ff6a --- /dev/null +++ b/tests/Images/Input/Jpg/issues/fuzz/Issue2085-NullReferenceException.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d478beff34179fda26238a44434607c276f55438ee96824c5af8c0188d358d8d +size 234 diff --git a/tests/Images/Input/Jpg/issues/issue-2056-exif-null-array.jpg b/tests/Images/Input/Jpg/issues/issue-2056-exif-null-array.jpg new file mode 100644 index 000000000..9b5bc8303 --- /dev/null +++ b/tests/Images/Input/Jpg/issues/issue-2056-exif-null-array.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4c52500be37a8ea1ee1caeb78c79e44b02e10912df4f6db65313c6745573c8ee +size 250451 diff --git a/tests/Images/Input/Jpg/progressive/Calliphora-arithmetic-progressive-interleaved.jpg b/tests/Images/Input/Jpg/progressive/Calliphora-arithmetic-progressive-interleaved.jpg new file mode 100644 index 000000000..91879221c --- /dev/null +++ b/tests/Images/Input/Jpg/progressive/Calliphora-arithmetic-progressive-interleaved.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bd688e22840892d7fd511782f3ff7043df1a3131fb17b000c6a3ca1d0e069950 +size 228527 diff --git a/tests/Images/Input/Png/AverageFilter4Bpp.png b/tests/Images/Input/Png/AverageFilter4Bpp.png new file mode 100644 index 000000000..728b6cfaf --- /dev/null +++ b/tests/Images/Input/Png/AverageFilter4Bpp.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7add6fba794bc76ccea2ee3a311b4050cf17f4f78b69a50785f7739b8b35919e +size 181108 diff --git a/tests/Images/Input/Png/PaethFilter4Bpp.png b/tests/Images/Input/Png/PaethFilter4Bpp.png new file mode 100644 index 000000000..64c9f96ec --- /dev/null +++ b/tests/Images/Input/Png/PaethFilter4Bpp.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8b2b0a1190854577d5181fe40af61c421d615a1a2727cf9be5ebe727eaafd00d +size 8624 diff --git a/tests/Images/Input/Png/SubFilter4Bpp.png b/tests/Images/Input/Png/SubFilter4Bpp.png new file mode 100644 index 000000000..d9f2c7fa2 --- /dev/null +++ b/tests/Images/Input/Png/SubFilter4Bpp.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:053fac72ff62c66dacb41a6251efa249d5b31567e0222efbf5b1bef912c0bf77 +size 13013 diff --git a/tests/Images/Input/Png/length_gama.png b/tests/Images/Input/Png/length_gama.png new file mode 100644 index 000000000..caf0fb01d --- /dev/null +++ b/tests/Images/Input/Png/length_gama.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:824766b34739727c722e88611d7b55401452c2970cd433f56e5f9f1b36d6950d +size 1285 diff --git a/tests/Images/Input/Png/missing_plte.png b/tests/Images/Input/Png/missing_plte.png new file mode 100644 index 000000000..0c24883fb --- /dev/null +++ b/tests/Images/Input/Png/missing_plte.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:73fd17a394f8258f4767986bc427c0160277819349c937f18cb29044e7549bc8 +size 506 diff --git a/tests/Images/Input/Png/missing_plte_2.png b/tests/Images/Input/Png/missing_plte_2.png new file mode 100644 index 000000000..8fc6580e5 --- /dev/null +++ b/tests/Images/Input/Png/missing_plte_2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:797844db61a937c6f31ecb392c8416fbf106017413ba55c6576e0b1fcfc1cf9c +size 597 diff --git a/tests/Images/Input/Png/transparency.png b/tests/Images/Input/Png/transparency.png new file mode 100644 index 000000000..26de0f2d1 --- /dev/null +++ b/tests/Images/Input/Png/transparency.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:843bea4db378f52935e2f19f60d289df8ebe20ddde3977c63225f1d58a10bd62 +size 48119 diff --git a/tests/Images/Input/Tiff/JpegCompressedGray.tiff b/tests/Images/Input/Tiff/JpegCompressedGray.tiff new file mode 100644 index 000000000..e7feed15a --- /dev/null +++ b/tests/Images/Input/Tiff/JpegCompressedGray.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:868afd018d025ed7636f1155c1b1f64ba8a36153b56c7598e8dee18ce770cd5a +size 539660 diff --git a/tests/Images/Input/Tiff/RgbaAlpha8bit.tiff b/tests/Images/Input/Tiff/RgbaAlpha8bit.tiff new file mode 100644 index 000000000..d0d9f2d1e --- /dev/null +++ b/tests/Images/Input/Tiff/RgbaAlpha8bit.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f8bc77670ea12cd163fbf21fa7dd6d6c10e3b8c1afc83f26618f92303d0cbfcf +size 235478 diff --git a/tests/Images/Input/Tiff/RgbaAssociatedAlpha10bit_lsb.tiff b/tests/Images/Input/Tiff/RgbaAssociatedAlpha10bit_lsb.tiff new file mode 100644 index 000000000..01ff24afc --- /dev/null +++ b/tests/Images/Input/Tiff/RgbaAssociatedAlpha10bit_lsb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1a9878183ebe84506810ea2edc6dc95cd76780f3847ac3a614ce2dcfb355ea9f +size 294278 diff --git a/tests/Images/Input/Tiff/RgbaAssociatedAlpha10bit_msb.tiff b/tests/Images/Input/Tiff/RgbaAssociatedAlpha10bit_msb.tiff new file mode 100644 index 000000000..a0eabbea2 --- /dev/null +++ b/tests/Images/Input/Tiff/RgbaAssociatedAlpha10bit_msb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b95334e1e4b78f5106cacd66d6dc9ad15d023c5449e9562b7489982c5336715 +size 294278 diff --git a/tests/Images/Input/Tiff/RgbaAssociatedAlpha12bit_lsb.tiff b/tests/Images/Input/Tiff/RgbaAssociatedAlpha12bit_lsb.tiff new file mode 100644 index 000000000..934895707 --- /dev/null +++ b/tests/Images/Input/Tiff/RgbaAssociatedAlpha12bit_lsb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b1aa42b51bd5474cbf333d9a0b89634198250d3eb5de4cf3af2a8d846395bf0 +size 353078 diff --git a/tests/Images/Input/Tiff/RgbaAssociatedAlpha12bit_msb.tiff b/tests/Images/Input/Tiff/RgbaAssociatedAlpha12bit_msb.tiff new file mode 100644 index 000000000..50e9d9c83 --- /dev/null +++ b/tests/Images/Input/Tiff/RgbaAssociatedAlpha12bit_msb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9a146f0bfcac158dfda1ba307737b39d472a95926ff172bc8c798557f2dd1e1b +size 353078 diff --git a/tests/Images/Input/Tiff/RgbaAssociatedAlpha14bit_lsb.tiff b/tests/Images/Input/Tiff/RgbaAssociatedAlpha14bit_lsb.tiff new file mode 100644 index 000000000..b58f097c1 --- /dev/null +++ b/tests/Images/Input/Tiff/RgbaAssociatedAlpha14bit_lsb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3190269edb721175e8f3788528bd84aee3a12b2c7fb970c02c98822452fc81b0 +size 411878 diff --git a/tests/Images/Input/Tiff/RgbaAssociatedAlpha14bit_msb.tiff b/tests/Images/Input/Tiff/RgbaAssociatedAlpha14bit_msb.tiff new file mode 100644 index 000000000..cfe66071d --- /dev/null +++ b/tests/Images/Input/Tiff/RgbaAssociatedAlpha14bit_msb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4d65b9b1172d125fece197c2bf332bff9074db2b443e74d1973a14dbe45865e8 +size 411878 diff --git a/tests/Images/Input/Tiff/RgbaAssociatedAlpha16bit_lsb.tiff b/tests/Images/Input/Tiff/RgbaAssociatedAlpha16bit_lsb.tiff new file mode 100644 index 000000000..a6199845a --- /dev/null +++ b/tests/Images/Input/Tiff/RgbaAssociatedAlpha16bit_lsb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5ee223455c3e2f748d4dc9a1446047adb3c547878b815504c785497d8c059d34 +size 470678 diff --git a/tests/Images/Input/Tiff/RgbaAssociatedAlpha16bit_msb.tiff b/tests/Images/Input/Tiff/RgbaAssociatedAlpha16bit_msb.tiff new file mode 100644 index 000000000..3bc030c50 --- /dev/null +++ b/tests/Images/Input/Tiff/RgbaAssociatedAlpha16bit_msb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9ea0206508fe23dfb8653ac1706894e54a4ad980b3bf75b68e5deb9f2838a1de +size 470678 diff --git a/tests/Images/Input/Tiff/RgbaAssociatedAlpha3bit.tiff b/tests/Images/Input/Tiff/RgbaAssociatedAlpha3bit.tiff new file mode 100644 index 000000000..7a87e5ef8 --- /dev/null +++ b/tests/Images/Input/Tiff/RgbaAssociatedAlpha3bit.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:03d1050a606b6fa9e909f54991ed12b1aa96b739e5f69f4b1bae7c903b1236ef +size 88478 diff --git a/tests/Images/Input/Tiff/RgbaAssociatedAlpha4bit.tiff b/tests/Images/Input/Tiff/RgbaAssociatedAlpha4bit.tiff new file mode 100644 index 000000000..2679e5829 --- /dev/null +++ b/tests/Images/Input/Tiff/RgbaAssociatedAlpha4bit.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:41481c59d9b63ca946d9d2cf2ccc599d1e48ef6fdfa00926c1e6d5cdfd35eb47 +size 117878 diff --git a/tests/Images/Input/Tiff/RgbaAssociatedAlpha5bit.tiff b/tests/Images/Input/Tiff/RgbaAssociatedAlpha5bit.tiff new file mode 100644 index 000000000..1cdb3d475 --- /dev/null +++ b/tests/Images/Input/Tiff/RgbaAssociatedAlpha5bit.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4bc3b2b8031593ff9a79d7caccd2ed614cf8d788dedbefbd27d0a4f88399be4e +size 147278 diff --git a/tests/Images/Input/Tiff/RgbaAssociatedAlpha6bit.tiff b/tests/Images/Input/Tiff/RgbaAssociatedAlpha6bit.tiff new file mode 100644 index 000000000..3184bd90b --- /dev/null +++ b/tests/Images/Input/Tiff/RgbaAssociatedAlpha6bit.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a9c1c64ce9a002337c3f6d332b4a5bb3016718b672f9c95d8792b013545553c2 +size 176678 diff --git a/tests/Images/Input/Tiff/RgbaUnassociatedAlpha10bit_lsb.tiff b/tests/Images/Input/Tiff/RgbaUnassociatedAlpha10bit_lsb.tiff new file mode 100644 index 000000000..f4a1a3f6b --- /dev/null +++ b/tests/Images/Input/Tiff/RgbaUnassociatedAlpha10bit_lsb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7f78ef11e4044d13ea3bf699e33472a708df3a5cc817dc41edb4df184f127f2b +size 294278 diff --git a/tests/Images/Input/Tiff/RgbaUnassociatedAlpha10bit_msb.tiff b/tests/Images/Input/Tiff/RgbaUnassociatedAlpha10bit_msb.tiff new file mode 100644 index 000000000..06beb72f3 --- /dev/null +++ b/tests/Images/Input/Tiff/RgbaUnassociatedAlpha10bit_msb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d9c2d6f4e16677d9fdfb38cc2bfb7df05eedbb8dc0e3c26a6dba9b427c2c698a +size 294278 diff --git a/tests/Images/Input/Tiff/RgbaUnassociatedAlpha12bit_lsb.tiff b/tests/Images/Input/Tiff/RgbaUnassociatedAlpha12bit_lsb.tiff new file mode 100644 index 000000000..94a944705 --- /dev/null +++ b/tests/Images/Input/Tiff/RgbaUnassociatedAlpha12bit_lsb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:53e9ff25da2a2a7a613328cfaf33799df51fe150586fb8de52070e8cc8830d97 +size 353078 diff --git a/tests/Images/Input/Tiff/RgbaUnassociatedAlpha12bit_msb.tiff b/tests/Images/Input/Tiff/RgbaUnassociatedAlpha12bit_msb.tiff new file mode 100644 index 000000000..26d911272 --- /dev/null +++ b/tests/Images/Input/Tiff/RgbaUnassociatedAlpha12bit_msb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:caff76e01bc39b7a295f01a11e3787a6487ac002af5586dd956166a9c91eb048 +size 353078 diff --git a/tests/Images/Input/Tiff/RgbaUnassociatedAlpha14bit_lsb.tiff b/tests/Images/Input/Tiff/RgbaUnassociatedAlpha14bit_lsb.tiff new file mode 100644 index 000000000..f1bd4c405 --- /dev/null +++ b/tests/Images/Input/Tiff/RgbaUnassociatedAlpha14bit_lsb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9193b6a194be970b2cfb26369fa487fd6ec2f1656af11df2e48f1d6b0971bbf8 +size 411878 diff --git a/tests/Images/Input/Tiff/RgbaUnassociatedAlpha14bit_msb.tiff b/tests/Images/Input/Tiff/RgbaUnassociatedAlpha14bit_msb.tiff new file mode 100644 index 000000000..b098877e0 --- /dev/null +++ b/tests/Images/Input/Tiff/RgbaUnassociatedAlpha14bit_msb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:888bc84af8dffc4565b215412a8a2bb56f0c78211a082b893d87595cd9f555c1 +size 411878 diff --git a/tests/Images/Input/Tiff/RgbaUnassociatedAlpha16bit_lsb.tiff b/tests/Images/Input/Tiff/RgbaUnassociatedAlpha16bit_lsb.tiff new file mode 100644 index 000000000..cfb5082f1 --- /dev/null +++ b/tests/Images/Input/Tiff/RgbaUnassociatedAlpha16bit_lsb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6eae92c012ad56c084929e0a2aff7c93091224d9f8ab7f52f71b845792d6b763 +size 470678 diff --git a/tests/Images/Input/Tiff/RgbaUnassociatedAlpha16bit_msb.tiff b/tests/Images/Input/Tiff/RgbaUnassociatedAlpha16bit_msb.tiff new file mode 100644 index 000000000..5cd4d9c45 --- /dev/null +++ b/tests/Images/Input/Tiff/RgbaUnassociatedAlpha16bit_msb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cbab54f221956215266c35bfd26fdfb123e092e3836e2401b9f24e1c5b23516e +size 470678 diff --git a/tests/Images/Input/Tiff/RgbaUnassociatedAlpha24bit_lsb.tiff b/tests/Images/Input/Tiff/RgbaUnassociatedAlpha24bit_lsb.tiff new file mode 100644 index 000000000..c0f54d865 --- /dev/null +++ b/tests/Images/Input/Tiff/RgbaUnassociatedAlpha24bit_lsb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fd9fa514619604275cede0b4747291db2f8e5ad02095565c891ace2b537d6336 +size 705878 diff --git a/tests/Images/Input/Tiff/RgbaUnassociatedAlpha24bit_msb.tiff b/tests/Images/Input/Tiff/RgbaUnassociatedAlpha24bit_msb.tiff new file mode 100644 index 000000000..33f7bee0f --- /dev/null +++ b/tests/Images/Input/Tiff/RgbaUnassociatedAlpha24bit_msb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:915ca9bbda952fc9ac78b44be07dab603948d51fb1a274935905e73cfe5bb0b9 +size 705878 diff --git a/tests/Images/Input/Tiff/RgbaUnassociatedAlpha2bit.tiff b/tests/Images/Input/Tiff/RgbaUnassociatedAlpha2bit.tiff new file mode 100644 index 000000000..cfbc05832 --- /dev/null +++ b/tests/Images/Input/Tiff/RgbaUnassociatedAlpha2bit.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f650c49faed4fd19b5527a0771489110090948e4ed33daa53b42c1776e288d89 +size 59078 diff --git a/tests/Images/Input/Tiff/RgbaUnassociatedAlpha32bit_lsb.tiff b/tests/Images/Input/Tiff/RgbaUnassociatedAlpha32bit_lsb.tiff new file mode 100644 index 000000000..0e11ef26c --- /dev/null +++ b/tests/Images/Input/Tiff/RgbaUnassociatedAlpha32bit_lsb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8cad4c5f42d77539ce1f67efa7e0ed1fa4f5dd32b3269e5862453d878c6b18d7 +size 941078 diff --git a/tests/Images/Input/Tiff/RgbaUnassociatedAlpha32bit_msb.tiff b/tests/Images/Input/Tiff/RgbaUnassociatedAlpha32bit_msb.tiff new file mode 100644 index 000000000..cfb8beb2b --- /dev/null +++ b/tests/Images/Input/Tiff/RgbaUnassociatedAlpha32bit_msb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8874322776b8620573c26a3c84b8c7c9bf0aeaa7d68a7fef009f8838d14dca5b +size 941078 diff --git a/tests/Images/Input/Tiff/RgbaUnassociatedAlpha3bit.tiff b/tests/Images/Input/Tiff/RgbaUnassociatedAlpha3bit.tiff new file mode 100644 index 000000000..aed59c331 --- /dev/null +++ b/tests/Images/Input/Tiff/RgbaUnassociatedAlpha3bit.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d0d89ddcda8525799b90c1cb4a15f3ea1cf399c2259017f219b1d09876161587 +size 88478 diff --git a/tests/Images/Input/Tiff/RgbaUnassociatedAlpha4bit.tiff b/tests/Images/Input/Tiff/RgbaUnassociatedAlpha4bit.tiff new file mode 100644 index 000000000..7176c382f --- /dev/null +++ b/tests/Images/Input/Tiff/RgbaUnassociatedAlpha4bit.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:be006e56c2c2f34686293e8a5f4397a7bf873ff927d4dd0808cac90310569254 +size 117878 diff --git a/tests/Images/Input/Tiff/RgbaUnassociatedAlpha5bit.tiff b/tests/Images/Input/Tiff/RgbaUnassociatedAlpha5bit.tiff new file mode 100644 index 000000000..aad945b8e --- /dev/null +++ b/tests/Images/Input/Tiff/RgbaUnassociatedAlpha5bit.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2928f06ef146625f5c696272901a63644699e3410dc155b7e4e470009a7f6dfc +size 147278 diff --git a/tests/Images/Input/Tiff/RgbaUnassociatedAlpha6bit.tiff b/tests/Images/Input/Tiff/RgbaUnassociatedAlpha6bit.tiff new file mode 100644 index 000000000..9909afe71 --- /dev/null +++ b/tests/Images/Input/Tiff/RgbaUnassociatedAlpha6bit.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:514ce84d3506aab7360b24f63aa1da1ea66abd9b1e534a12487d03486a7e593b +size 176678 diff --git a/tests/Images/Input/Tiff/RgbaUnassociatedAlpha8bit.tiff b/tests/Images/Input/Tiff/RgbaUnassociatedAlpha8bit.tiff new file mode 100644 index 000000000..47270d98d --- /dev/null +++ b/tests/Images/Input/Tiff/RgbaUnassociatedAlpha8bit.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:234401d70156cc748a67992919f8780bb855bc5e87e404b573f61b5eb4817dcf +size 266816 diff --git a/tests/Images/Input/Tiff/RgbaUnassociatedAlphaPlanar16bit_lsb.tiff b/tests/Images/Input/Tiff/RgbaUnassociatedAlphaPlanar16bit_lsb.tiff new file mode 100644 index 000000000..a24d74c19 --- /dev/null +++ b/tests/Images/Input/Tiff/RgbaUnassociatedAlphaPlanar16bit_lsb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6bc4325ce7be8a16d23c559300c79c3228c2f5a4c266844ba49763a32d29f10e +size 470710 diff --git a/tests/Images/Input/Tiff/RgbaUnassociatedAlphaPlanar16bit_msb.tiff b/tests/Images/Input/Tiff/RgbaUnassociatedAlphaPlanar16bit_msb.tiff new file mode 100644 index 000000000..b7393cb22 --- /dev/null +++ b/tests/Images/Input/Tiff/RgbaUnassociatedAlphaPlanar16bit_msb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:08f8c284f7a9a6f362c09a418d85a94a1fe09bfc3f4cfe6859a82d6814718091 +size 470710 diff --git a/tests/Images/Input/Tiff/RgbaUnassociatedAlphaPlanar24bit_lsb.tiff b/tests/Images/Input/Tiff/RgbaUnassociatedAlphaPlanar24bit_lsb.tiff new file mode 100644 index 000000000..fd4171158 --- /dev/null +++ b/tests/Images/Input/Tiff/RgbaUnassociatedAlphaPlanar24bit_lsb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ba4da7d63931f4462113e00bdee9e66e333ca42a47a33f62057c248bf4696ef +size 705910 diff --git a/tests/Images/Input/Tiff/RgbaUnassociatedAlphaPlanar24bit_msb.tiff b/tests/Images/Input/Tiff/RgbaUnassociatedAlphaPlanar24bit_msb.tiff new file mode 100644 index 000000000..7aa6beeaa --- /dev/null +++ b/tests/Images/Input/Tiff/RgbaUnassociatedAlphaPlanar24bit_msb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:862ec9153cc755aa3ece8965a9d001a630ff756dfb018a9474661730482964cb +size 705910 diff --git a/tests/Images/Input/Tiff/RgbaUnassociatedAlphaPlanar32bit_lsb.tiff b/tests/Images/Input/Tiff/RgbaUnassociatedAlphaPlanar32bit_lsb.tiff new file mode 100644 index 000000000..fb5dd1dcc --- /dev/null +++ b/tests/Images/Input/Tiff/RgbaUnassociatedAlphaPlanar32bit_lsb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc4f67dacd3262418769831aeabf99c9a88a9674fabf9a08c8b3d3e47ac6d07a +size 941110 diff --git a/tests/Images/Input/Tiff/RgbaUnassociatedAlphaPlanar32bit_msb.tiff b/tests/Images/Input/Tiff/RgbaUnassociatedAlphaPlanar32bit_msb.tiff new file mode 100644 index 000000000..6c45e96fa --- /dev/null +++ b/tests/Images/Input/Tiff/RgbaUnassociatedAlphaPlanar32bit_msb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a98a0176f5db7af5b22c74f4a518c2df1055b5ca7e732f63426b3df8090fc313 +size 941110 diff --git a/tests/Images/Input/Tiff/RgbaUnassociatedAlphaPlanar8bit.tiff b/tests/Images/Input/Tiff/RgbaUnassociatedAlphaPlanar8bit.tiff new file mode 100644 index 000000000..f853cbc62 --- /dev/null +++ b/tests/Images/Input/Tiff/RgbaUnassociatedAlphaPlanar8bit.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2185ef9a84701bcff104d4f2fe40171ed853e5d02a2049b209ee2a4c65105ea9 +size 235502 diff --git a/tests/Images/Input/Tiff/RgbaUnassociatedAlphaPredictor16bit_lsb.tiff b/tests/Images/Input/Tiff/RgbaUnassociatedAlphaPredictor16bit_lsb.tiff new file mode 100644 index 000000000..9d985a33a --- /dev/null +++ b/tests/Images/Input/Tiff/RgbaUnassociatedAlphaPredictor16bit_lsb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:874ef7a59491ba68364312b7bc27b6620d15ce8b1d5b780f57c6e6d8b919ef1f +size 73922 diff --git a/tests/Images/Input/Tiff/RgbaUnassociatedAlphaPredictor16bit_msb.tiff b/tests/Images/Input/Tiff/RgbaUnassociatedAlphaPredictor16bit_msb.tiff new file mode 100644 index 000000000..b195cc15a --- /dev/null +++ b/tests/Images/Input/Tiff/RgbaUnassociatedAlphaPredictor16bit_msb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d4faa8617d10ea5f79225c528c0a6d5c36f73d315e46150703df5ca5008ea1bd +size 73922 diff --git a/tests/Images/Input/Tiff/RgbaUnassociatedAlphaPredictor32bit_lsb.tiff b/tests/Images/Input/Tiff/RgbaUnassociatedAlphaPredictor32bit_lsb.tiff new file mode 100644 index 000000000..492a8fae8 --- /dev/null +++ b/tests/Images/Input/Tiff/RgbaUnassociatedAlphaPredictor32bit_lsb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a17791068b9c3eb40db3157a9103892aaf4a5a74072c93006bfa702ba5545e5 +size 80428 diff --git a/tests/Images/Input/Tiff/RgbaUnassociatedAlphaPredictor32bit_msb.tiff b/tests/Images/Input/Tiff/RgbaUnassociatedAlphaPredictor32bit_msb.tiff new file mode 100644 index 000000000..43075dc21 --- /dev/null +++ b/tests/Images/Input/Tiff/RgbaUnassociatedAlphaPredictor32bit_msb.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:63fef29d79f8d707c74b6e083de6bb2ad41dde1d9b1aea5bd7729a2f7399132e +size 80344 diff --git a/tests/Images/Input/Tiff/RgbaUnassociatedAlphaPredictor8bit.tiff b/tests/Images/Input/Tiff/RgbaUnassociatedAlphaPredictor8bit.tiff new file mode 100644 index 000000000..557b6216a --- /dev/null +++ b/tests/Images/Input/Tiff/RgbaUnassociatedAlphaPredictor8bit.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d91f0740d6df983b5e5fe904c22fe86c2a7ffd86673fb078092d80c96359fc1 +size 53666 diff --git a/tests/Images/Input/Tiff/SKC1H3.tiff b/tests/Images/Input/Tiff/SKC1H3.tiff new file mode 100644 index 000000000..9f9a50fdd --- /dev/null +++ b/tests/Images/Input/Tiff/SKC1H3.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:938bbf1c0f8bdbea0c632bb8d51c1150f757f88b3779d7fa18c296a3a3f61e9b +size 13720193 diff --git a/tests/Images/Input/Tiff/flower-rgb-3bit.tiff b/tests/Images/Input/Tiff/flower-rgb-3bit.tiff new file mode 100644 index 000000000..db5f7916c --- /dev/null +++ b/tests/Images/Input/Tiff/flower-rgb-3bit.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3b778a97467b1475c47c71b5f029c23b0962309095b8702cfc81c8fbaf4b8edb +size 3886 diff --git a/tests/Images/Input/Tiff/flower-rgb-5bit.tiff b/tests/Images/Input/Tiff/flower-rgb-5bit.tiff new file mode 100644 index 000000000..af1bc3921 --- /dev/null +++ b/tests/Images/Input/Tiff/flower-rgb-5bit.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44a53ffce2bfd3f1010a1fe232c8010708458880230f11b75ea3ef16b5164ce1 +size 6208 diff --git a/tests/Images/Input/Tiff/flower-rgb-6bit.tiff b/tests/Images/Input/Tiff/flower-rgb-6bit.tiff new file mode 100644 index 000000000..b0399487d --- /dev/null +++ b/tests/Images/Input/Tiff/flower-rgb-6bit.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d1db5b448aa9d61dd38dfb86e91e04fd0779a9c68cc2073913bcee3c635b5fe +size 7412 diff --git a/tests/Images/Input/Webp/exif_lossy.webp b/tests/Images/Input/Webp/exif_lossy.webp index 35e454b96..5d6db3800 100644 --- a/tests/Images/Input/Webp/exif_lossy.webp +++ b/tests/Images/Input/Webp/exif_lossy.webp @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fdf4e9b20af4168f4177d33f7f502906343bbaaae2af9b90e1531bd4452b317b -size 40765 +oid sha256:5c53967bfefcfece8cd4411740c1c394e75864ca61a7a9751df3b28e727c0205 +size 68646 diff --git a/tests/Images/Input/Webp/exif_lossy_not_enough_data.webp b/tests/Images/Input/Webp/exif_lossy_not_enough_data.webp new file mode 100644 index 000000000..35e454b96 --- /dev/null +++ b/tests/Images/Input/Webp/exif_lossy_not_enough_data.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fdf4e9b20af4168f4177d33f7f502906343bbaaae2af9b90e1531bd4452b317b +size 40765