diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 0943ab4eb7..ffacf51e4a 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 024de8e19e..0000000000 --- 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 0000000000..e96677b24b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/commercial-bug-report.yml @@ -0,0 +1,42 @@ +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: 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/oss-bug-report.md b/.github/ISSUE_TEMPLATE/oss-bug-report.md deleted file mode 100644 index 9e9567a99a..0000000000 --- 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 0000000000..00138d511e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/oss-bug-report.yml @@ -0,0 +1,40 @@ +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: 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 5189f04356..43a4a17084 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -1,202 +1,198 @@ name: Build on: - push: - branches: - - master - tags: - - "v*" - pull_request: - branches: - - master + push: + branches: + - main + tags: + - "v*" + pull_request: + branches: + - main jobs: - Build: - strategy: - matrix: - options: - - os: ubuntu-latest - framework: net6.0 - sdk: 6.0.x - sdk-preview: true - runtime: -x64 - codecov: false - - os: macos-latest - framework: net6.0 - sdk: 6.0.x - sdk-preview: true - runtime: -x64 - codecov: false - - os: windows-latest - framework: net6.0 - sdk: 6.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 - runtime: -x64 - codecov: true - - 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 - runtime: -x64 - codecov: false - - os: windows-latest - framework: net472 - runtime: -x64 - codecov: false - - os: windows-latest - framework: net472 - runtime: -x86 - codecov: false - - runs-on: ${{matrix.options.os}} - - steps: - - name: Git Config - shell: bash - run: | - git config --global core.autocrlf false - git config --global core.longpaths true - - - name: Git Checkout - uses: actions/checkout@v2 - with: - fetch-depth: 0 - submodules: recursive - - # See https://github.com/actions/checkout/issues/165#issuecomment-657673315 - - name: Git Create LFS FileList - run: git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id - - - name: Git Setup LFS Cache - uses: actions/cache@v2 - id: lfs-cache - with: - path: .git/lfs - key: ${{ runner.os }}-lfs-${{ hashFiles('.lfs-assets-id') }}-v1 - - - name: Git Pull LFS - run: git lfs pull - - - name: NuGet Install - uses: NuGet/setup-nuget@v1 - - - name: NuGet Setup Cache - uses: actions/cache@v2 - id: nuget-cache - with: - path: ~/.nuget - key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/*.props', '**/*.targets') }} - restore-keys: ${{ runner.os }}-nuget- - - - name: DotNet Setup Preview - if: ${{ matrix.options.sdk-preview == true }} - uses: actions/setup-dotnet@v1 - with: - dotnet-version: ${{ matrix.options.sdk }} - include-prerelease: true - - - name: DotNet Build - if: ${{ matrix.options.sdk-preview != true }} - shell: pwsh - run: ./ci-build.ps1 "${{matrix.options.framework}}" - env: - SIXLABORS_TESTING: True - - - name: DotNet Build Preview - if: ${{ matrix.options.sdk-preview == true }} - shell: pwsh - run: ./ci-build.ps1 "${{matrix.options.framework}}" - env: - SIXLABORS_TESTING_PREVIEW: True - - - name: DotNet Test - if: ${{ matrix.options.sdk-preview != true }} - shell: pwsh - run: ./ci-test.ps1 "${{matrix.options.os}}" "${{matrix.options.framework}}" "${{matrix.options.runtime}}" "${{matrix.options.codecov}}" - env: - SIXLABORS_TESTING: True - XUNIT_PATH: .\tests\ImageSharp.Tests # Required for xunit - - - name: DotNet Test Preview - if: ${{ matrix.options.sdk-preview == true }} - shell: pwsh - run: ./ci-test.ps1 "${{matrix.options.os}}" "${{matrix.options.framework}}" "${{matrix.options.runtime}}" "${{matrix.options.codecov}}" - env: - SIXLABORS_TESTING_PREVIEW: True - XUNIT_PATH: .\tests\ImageSharp.Tests # Required for xunit - - - name: Export Failed Output - uses: actions/upload-artifact@v2 - if: failure() - with: - name: actual_output_${{ runner.os }}_${{ matrix.options.framework }}${{ matrix.options.runtime }}.zip - path: tests/Images/ActualOutput/ - - - name: Codecov Update - uses: codecov/codecov-action@v1 - if: matrix.options.codecov == true && startsWith(github.repository, 'SixLabors') - with: - flags: unittests - - Publish: - needs: [Build] - - runs-on: ubuntu-latest - - if: (github.event_name == 'push') - - steps: - - name: Git Config - shell: bash - run: | - git config --global core.autocrlf false - git config --global core.longpaths true - - - name: Git Checkout - uses: actions/checkout@v2 - with: - fetch-depth: 0 - submodules: recursive - - - name: NuGet Install - uses: NuGet/setup-nuget@v1 - - - name: NuGet Setup Cache - uses: actions/cache@v2 - id: nuget-cache - with: - path: ~/.nuget - key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/*.props', '**/*.targets') }} - restore-keys: ${{ runner.os }}-nuget- - - - name: DotNet Pack - shell: pwsh - run: ./ci-pack.ps1 - - - name: MyGet Publish - shell: pwsh - run: | - dotnet nuget push .\artifacts\*.nupkg -k ${{secrets.MYGET_TOKEN}} -s https://www.myget.org/F/sixlabors/api/v2/package - dotnet nuget push .\artifacts\*.snupkg -k ${{secrets.MYGET_TOKEN}} -s https://www.myget.org/F/sixlabors/api/v3/index.json - # TODO: If github.ref starts with 'refs/tags' then it was tag push and we can optionally push out package to nuget.org + Build: + strategy: + matrix: + options: + - os: ubuntu-latest + framework: net6.0 + sdk: 6.0.x + sdk-preview: true + runtime: -x64 + codecov: false + - os: macos-latest + framework: net6.0 + sdk: 6.0.x + sdk-preview: true + runtime: -x64 + codecov: false + - os: windows-latest + framework: net6.0 + sdk: 6.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 + 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 + runtime: -x64 + codecov: false + - os: windows-latest + framework: net472 + runtime: -x64 + codecov: false + - os: windows-latest + framework: net472 + runtime: -x86 + codecov: false + + runs-on: ${{matrix.options.os}} + + steps: + - name: Git Config + shell: bash + run: | + git config --global core.autocrlf false + git config --global core.longpaths true + + - name: Git Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + submodules: recursive + + # See https://github.com/actions/checkout/issues/165#issuecomment-657673315 + - name: Git Create LFS FileList + run: git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id + + - name: Git Setup LFS Cache + uses: actions/cache@v2 + id: lfs-cache + with: + path: .git/lfs + key: ${{ runner.os }}-lfs-${{ hashFiles('.lfs-assets-id') }}-v1 + + - name: Git Pull LFS + run: git lfs pull + + - name: NuGet Install + uses: NuGet/setup-nuget@v1 + + - name: NuGet Setup Cache + uses: actions/cache@v2 + id: nuget-cache + with: + path: ~/.nuget + 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 + 5.0.x + 3.1.x + 2.1.x + + - name: DotNet Build + if: ${{ matrix.options.sdk-preview != true }} + shell: pwsh + run: ./ci-build.ps1 "${{matrix.options.framework}}" + env: + SIXLABORS_TESTING: True + + - name: DotNet Build Preview + if: ${{ matrix.options.sdk-preview == true }} + shell: pwsh + run: ./ci-build.ps1 "${{matrix.options.framework}}" + env: + SIXLABORS_TESTING_PREVIEW: True + + - name: DotNet Test + if: ${{ matrix.options.sdk-preview != true }} + shell: pwsh + run: ./ci-test.ps1 "${{matrix.options.os}}" "${{matrix.options.framework}}" "${{matrix.options.runtime}}" "${{matrix.options.codecov}}" + env: + SIXLABORS_TESTING: True + XUNIT_PATH: .\tests\ImageSharp.Tests # Required for xunit + + - name: DotNet Test Preview + if: ${{ matrix.options.sdk-preview == true }} + shell: pwsh + run: ./ci-test.ps1 "${{matrix.options.os}}" "${{matrix.options.framework}}" "${{matrix.options.runtime}}" "${{matrix.options.codecov}}" + env: + SIXLABORS_TESTING_PREVIEW: True + XUNIT_PATH: .\tests\ImageSharp.Tests # Required for xunit + + - name: Export Failed Output + uses: actions/upload-artifact@v2 + if: failure() + with: + name: actual_output_${{ runner.os }}_${{ matrix.options.framework }}${{ matrix.options.runtime }}.zip + path: tests/Images/ActualOutput/ + + Publish: + needs: [Build] + + runs-on: ubuntu-latest + + if: (github.event_name == 'push') + + steps: + - name: Git Config + shell: bash + run: | + git config --global core.autocrlf false + git config --global core.longpaths true + + - name: Git Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + submodules: recursive + + - name: NuGet Install + uses: NuGet/setup-nuget@v1 + + - name: NuGet Setup Cache + uses: actions/cache@v2 + id: nuget-cache + with: + path: ~/.nuget + key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/*.props', '**/*.targets') }} + restore-keys: ${{ runner.os }}-nuget- + + - name: DotNet Pack + shell: pwsh + run: ./ci-pack.ps1 + + - name: MyGet Publish + shell: pwsh + run: | + dotnet nuget push .\artifacts\*.nupkg -k ${{secrets.MYGET_TOKEN}} -s https://www.myget.org/F/sixlabors/api/v2/package + dotnet nuget push .\artifacts\*.snupkg -k ${{secrets.MYGET_TOKEN}} -s https://www.myget.org/F/sixlabors/api/v3/index.json + # TODO: If github.ref starts with 'refs/tags' then it was tag push and we can optionally push out package to nuget.org diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml new file mode 100644 index 0000000000..2b14f2a4b7 --- /dev/null +++ b/.github/workflows/code-coverage.yml @@ -0,0 +1,81 @@ +name: CodeCoverage + +on: + schedule: + # 2AM every Tuesday/Thursday + - cron: "0 2 * * 2,4" +jobs: + Build: + strategy: + matrix: + options: + - os: ubuntu-latest + framework: netcoreapp3.1 + runtime: -x64 + codecov: true + + runs-on: ${{matrix.options.os}} + + steps: + - name: Git Config + shell: bash + run: | + git config --global core.autocrlf false + git config --global core.longpaths true + + - name: Git Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + submodules: recursive + + # See https://github.com/actions/checkout/issues/165#issuecomment-657673315 + - name: Git Create LFS FileList + run: git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id + + - name: Git Setup LFS Cache + uses: actions/cache@v2 + id: lfs-cache + with: + path: .git/lfs + key: ${{ runner.os }}-lfs-${{ hashFiles('.lfs-assets-id') }}-v1 + + - name: Git Pull LFS + run: git lfs pull + + - name: NuGet Install + uses: NuGet/setup-nuget@v1 + + - name: NuGet Setup Cache + uses: actions/cache@v2 + id: nuget-cache + with: + path: ~/.nuget + key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/*.props', '**/*.targets') }} + restore-keys: ${{ runner.os }}-nuget- + + - name: DotNet Build + shell: pwsh + run: ./ci-build.ps1 "${{matrix.options.framework}}" + env: + SIXLABORS_TESTING: True + + - name: DotNet Test + shell: pwsh + run: ./ci-test.ps1 "${{matrix.options.os}}" "${{matrix.options.framework}}" "${{matrix.options.runtime}}" "${{matrix.options.codecov}}" + env: + SIXLABORS_TESTING: True + XUNIT_PATH: .\tests\ImageSharp.Tests # Required for xunit + + - name: Export Failed Output + uses: actions/upload-artifact@v2 + if: failure() + with: + name: actual_output_${{ runner.os }}_${{ matrix.options.framework }}${{ matrix.options.runtime }}.zip + path: tests/Images/ActualOutput/ + + - name: Codecov Update + uses: codecov/codecov-action@v1 + if: matrix.options.codecov == true && startsWith(github.repository, 'SixLabors') + with: + flags: unittests diff --git a/ImageSharp.sln b/ImageSharp.sln index f16f98ac59..17d293b434 100644 --- a/ImageSharp.sln +++ b/ImageSharp.sln @@ -551,6 +551,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{C0D7754B-5277-438E-ABEB-2BA34401B5A7}" ProjectSection(SolutionItems) = preProject .github\workflows\build-and-test.yml = .github\workflows\build-and-test.yml + .github\workflows\code-coverage.yml = .github\workflows\code-coverage.yml EndProjectSection EndProject Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "SharedInfrastructure", "shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.shproj", "{68A8CC40-6AED-4E96-B524-31B1158FDEEA}" diff --git a/README.md b/README.md index fdf14b4963..2492041db9 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) @@ -39,12 +39,12 @@ 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! +- 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 +93,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/ImageSharp/Common/Helpers/HexConverter.cs b/src/ImageSharp/Common/Helpers/HexConverter.cs new file mode 100644 index 0000000000..c55e9bbd9d --- /dev/null +++ b/src/ImageSharp/Common/Helpers/HexConverter.cs @@ -0,0 +1,98 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Common.Helpers +{ + internal static class HexConverter + { + /// + /// Parses a hexadecimal string into a byte array without allocations. Throws on non-hexadecimal character. + /// Adapted from https://source.dot.net/#System.Private.CoreLib/Convert.cs,c9e4fbeaca708991. + /// + /// The hexadecimal string to parse. + /// The destination for the parsed bytes. Must be at least .Length / 2 bytes long. + /// The number of bytes written to . + public static int HexStringToBytes(ReadOnlySpan chars, Span bytes) + { + if ((chars.Length % 2) != 0) + { + throw new ArgumentException("Input string length must be a multiple of 2", nameof(chars)); + } + + if ((bytes.Length * 2) < chars.Length) + { + throw new ArgumentException("Output span must be at least half the length of the input string"); + } + else + { + // Slightly better performance in the loop below, allows us to skip a bounds check + // while still supporting output buffers that are larger than necessary + bytes = bytes.Slice(0, chars.Length / 2); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static int FromChar(int c) + { + // Map from an ASCII char to its hex value, e.g. arr['b'] == 11. 0xFF means it's not a hex digit. + // This doesn't actually allocate. + ReadOnlySpan charToHexLookup = new byte[] + { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 15 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 31 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 47 + 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 63 + 0xFF, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 79 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 95 + 0xFF, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 111 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 127 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 143 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 159 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 175 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 191 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 207 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 223 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 239 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 255 + }; + + return c >= charToHexLookup.Length ? 0xFF : charToHexLookup[c]; + } + + // See https://source.dot.net/#System.Private.CoreLib/HexConverter.cs,4681d45a0aa0b361 + int i = 0; + int j = 0; + int byteLo = 0; + int byteHi = 0; + while (j < bytes.Length) + { + byteLo = FromChar(chars[i + 1]); + byteHi = FromChar(chars[i]); + + // byteHi hasn't been shifted to the high half yet, so the only way the bitwise or produces this pattern + // is if either byteHi or byteLo was not a hex character. + if ((byteLo | byteHi) == 0xFF) + { + break; + } + + bytes[j++] = (byte)((byteHi << 4) | byteLo); + i += 2; + } + + if (byteLo == 0xFF) + { + i++; + } + + if ((byteLo | byteHi) == 0xFF) + { + throw new ArgumentException("Input string contained non-hexadecimal characters", nameof(chars)); + } + + return j; + } + } +} diff --git a/src/ImageSharp/Configuration.cs b/src/ImageSharp/Configuration.cs index a3e0c99489..b19d31b4e7 100644 --- a/src/ImageSharp/Configuration.cs +++ b/src/ImageSharp/Configuration.cs @@ -123,25 +123,25 @@ namespace SixLabors.ImageSharp public ReadOrigin ReadOrigin { get; set; } = ReadOrigin.Current; /// - /// Gets or sets the that is currently in use. + /// Gets or the that is currently in use. /// - public ImageFormatManager ImageFormatsManager { get; set; } = new(); + public ImageFormatManager ImageFormatsManager { get; private set; } = new ImageFormatManager(); /// - /// Gets or sets the that is currently in use. - /// Defaults to . + /// Gets or sets the that is currently in use. + /// Defaults to . /// /// Allocators are expensive, so it is strongly recommended to use only one busy instance per process. /// In case you need to customize it, you can ensure this by changing /// /// /// It's possible to reduce allocator footprint by assigning a custom instance created with - /// , but note that since the default pooling + /// , but note that since the default pooling /// allocators are expensive, it is strictly recommended to use a single process-wide allocator. /// You can ensure this by altering the allocator of , or by implementing custom application logic that /// manages allocator lifetime. /// - /// If an allocator has to be dropped for some reason, + /// If an allocator has to be dropped for some reason, /// shall be invoked after disposing all associated instances. /// public MemoryAllocator MemoryAllocator diff --git a/src/ImageSharp/Diagnostics/MemoryDiagnostics.cs b/src/ImageSharp/Diagnostics/MemoryDiagnostics.cs new file mode 100644 index 0000000000..d83e737f12 --- /dev/null +++ b/src/ImageSharp/Diagnostics/MemoryDiagnostics.cs @@ -0,0 +1,83 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +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--; + } + } + } + + /// + /// 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); + + internal static void DecrementTotalUndisposedAllocationCount() => + Interlocked.Decrement(ref totalUndisposedAllocationCount); + + 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/Gif/GifConstants.cs b/src/ImageSharp/Formats/Gif/GifConstants.cs index 24fd8a9365..1179b67b1e 100644 --- a/src/ImageSharp/Formats/Gif/GifConstants.cs +++ b/src/ImageSharp/Formats/Gif/GifConstants.cs @@ -121,5 +121,16 @@ namespace SixLabors.ImageSharp.Formats.Gif (byte)'P', (byte)'E', (byte)'2', (byte)'.', (byte)'0' }; + + /// + /// Gets the ASCII encoded application identification bytes. + /// + internal static ReadOnlySpan XmpApplicationIdentificationBytes => new[] + { + (byte)'X', (byte)'M', (byte)'P', + (byte)' ', (byte)'D', (byte)'a', + (byte)'t', (byte)'a', + (byte)'X', (byte)'M', (byte)'P' + }; } } diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index 3e33a6e379..72e3ed144c 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -11,6 +11,7 @@ using System.Threading; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Gif @@ -250,7 +251,7 @@ namespace SixLabors.ImageSharp.Formats.Gif } /// - /// Reads the application extension block parsing any animation information + /// Reads the application extension block parsing any animation or XMP information /// if present. /// private void ReadApplicationExtension() @@ -258,25 +259,37 @@ namespace SixLabors.ImageSharp.Formats.Gif int appLength = this.stream.ReadByte(); // If the length is 11 then it's a valid extension and most likely - // a NETSCAPE or ANIMEXTS extension. We want the loop count from this. + // a NETSCAPE, XMP or ANIMEXTS extension. We want the loop count from this. if (appLength == GifConstants.ApplicationBlockSize) { - this.stream.Skip(appLength); - int subBlockSize = this.stream.ReadByte(); + this.stream.Read(this.buffer, 0, GifConstants.ApplicationBlockSize); + bool isXmp = this.buffer.AsSpan().StartsWith(GifConstants.XmpApplicationIdentificationBytes); - // TODO: There's also a NETSCAPE buffer extension. - // http://www.vurdalakov.net/misc/gif/netscape-buffering-application-extension - if (subBlockSize == GifConstants.NetscapeLoopingSubBlockSize) + if (isXmp) { - this.stream.Read(this.buffer, 0, GifConstants.NetscapeLoopingSubBlockSize); - this.gifMetadata.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.AsSpan(1)).RepeatCount; - this.stream.Skip(1); // Skip the terminator. + var extension = GifXmpApplicationExtension.Read(this.stream); + this.metadata.XmpProfile = new XmpProfile(extension.Data); return; } + else + { + int subBlockSize = this.stream.ReadByte(); + + // TODO: There's also a NETSCAPE buffer extension. + // http://www.vurdalakov.net/misc/gif/netscape-buffering-application-extension + if (subBlockSize == GifConstants.NetscapeLoopingSubBlockSize) + { + this.stream.Read(this.buffer, 0, GifConstants.NetscapeLoopingSubBlockSize); + this.gifMetadata.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.AsSpan(1)).RepeatCount; + this.stream.Skip(1); // Skip the terminator. + return; + } + + // Could be something else not supported yet. + // Skip the subblock and terminator. + this.SkipBlock(subBlockSize); + } - // Could be XMP or something else not supported yet. - // Skip the subblock and terminator. - this.SkipBlock(subBlockSize); return; } @@ -363,7 +376,17 @@ 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 @@ -402,6 +425,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; @@ -409,8 +433,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); @@ -432,6 +463,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 @@ -439,7 +475,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; @@ -622,10 +657,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 05ea14e9ce..a21b050a81 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -10,6 +10,7 @@ using System.Threading; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing.Processors.Quantization; @@ -121,11 +122,8 @@ namespace SixLabors.ImageSharp.Formats.Gif // Write the comments. this.WriteComments(gifMetadata, stream); - // Write application extension to allow additional frames. - if (image.Frames.Count > 1) - { - this.WriteApplicationExtension(stream, gifMetadata.RepeatCount); - } + // Write application extensions. + this.WriteApplicationExtensions(stream, image.Frames.Count, gifMetadata.RepeatCount, metadata.XmpProfile); if (useGlobalTable) { @@ -326,15 +324,24 @@ namespace SixLabors.ImageSharp.Formats.Gif /// Writes the application extension to the stream. /// /// The stream to write to. + /// The frame count fo this image. /// The animated image repeat count. - private void WriteApplicationExtension(Stream stream, ushort repeatCount) + /// The XMP metadata profile. Null if profile is not to be written. + private void WriteApplicationExtensions(Stream stream, int frameCount, ushort repeatCount, XmpProfile xmpProfile) { - // Application Extension Header - if (repeatCount != 1) + // Application Extension: Loop repeat count. + if (frameCount > 1 && repeatCount != 1) { var loopingExtension = new GifNetscapeLoopingApplicationExtension(repeatCount); this.WriteExtension(loopingExtension, stream); } + + // Application Extension: XMP Profile. + if (xmpProfile != null) + { + var xmpExtension = new GifXmpApplicationExtension(xmpProfile.Data); + this.WriteExtension(xmpExtension, stream); + } } /// @@ -420,14 +427,28 @@ namespace SixLabors.ImageSharp.Formats.Gif private void WriteExtension(TGifExtension extension, Stream stream) where TGifExtension : struct, IGifExtension { - this.buffer[0] = GifConstants.ExtensionIntroducer; - this.buffer[1] = extension.Label; + IMemoryOwner owner = null; + Span buffer; + int extensionSize = extension.ContentLength; + if (extensionSize > this.buffer.Length - 3) + { + owner = this.memoryAllocator.Allocate(extensionSize + 3); + buffer = owner.GetSpan(); + } + else + { + buffer = this.buffer; + } + + buffer[0] = GifConstants.ExtensionIntroducer; + buffer[1] = extension.Label; - int extensionSize = extension.WriteTo(this.buffer.AsSpan(2)); + extension.WriteTo(buffer.Slice(2)); - this.buffer[extensionSize + 2] = GifConstants.Terminator; + buffer[extensionSize + 2] = GifConstants.Terminator; - stream.Write(this.buffer, 0, extensionSize + 3); + stream.Write(buffer, 0, extensionSize + 3); + owner?.Dispose(); } /// diff --git a/src/ImageSharp/Formats/Gif/Sections/GifGraphicControlExtension.cs b/src/ImageSharp/Formats/Gif/Sections/GifGraphicControlExtension.cs index ee5a43d805..801849c9b8 100644 --- a/src/ImageSharp/Formats/Gif/Sections/GifGraphicControlExtension.cs +++ b/src/ImageSharp/Formats/Gif/Sections/GifGraphicControlExtension.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. using System; @@ -63,6 +63,8 @@ namespace SixLabors.ImageSharp.Formats.Gif byte IGifExtension.Label => GifConstants.GraphicControlLabel; + int IGifExtension.ContentLength => 5; + public int WriteTo(Span buffer) { ref GifGraphicControlExtension dest = ref Unsafe.As(ref MemoryMarshal.GetReference(buffer)); diff --git a/src/ImageSharp/Formats/Gif/Sections/GifNetscapeLoopingApplicationExtension.cs b/src/ImageSharp/Formats/Gif/Sections/GifNetscapeLoopingApplicationExtension.cs index 26faa8925e..2c7bed6115 100644 --- a/src/ImageSharp/Formats/Gif/Sections/GifNetscapeLoopingApplicationExtension.cs +++ b/src/ImageSharp/Formats/Gif/Sections/GifNetscapeLoopingApplicationExtension.cs @@ -12,6 +12,8 @@ namespace SixLabors.ImageSharp.Formats.Gif public byte Label => GifConstants.ApplicationExtensionLabel; + public int ContentLength => 16; + /// /// Gets the repeat count. /// 0 means loop indefinitely. Count is set as play n + 1 times. diff --git a/src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs b/src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs new file mode 100644 index 0000000000..236508fe99 --- /dev/null +++ b/src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs @@ -0,0 +1,97 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Collections.Generic; +using System.IO; +using SixLabors.ImageSharp.Metadata.Profiles.Xmp; + +namespace SixLabors.ImageSharp.Formats.Gif +{ + internal readonly struct GifXmpApplicationExtension : IGifExtension + { + public GifXmpApplicationExtension(byte[] data) => this.Data = data; + + public byte Label => GifConstants.ApplicationExtensionLabel; + + public int ContentLength => this.Data.Length + 269; // 12 + Data Length + 1 + 256 + + /// + /// Gets the raw Data. + /// + public byte[] Data { get; } + + /// + /// Reads the XMP metadata from the specified stream. + /// + /// The stream to read from. + /// The XMP metadata + /// Thrown if the XMP block is not properly terminated. + public static GifXmpApplicationExtension Read(Stream stream) + { + // 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); + } + + // 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++) + { + list[j].CopyTo(bufferSpan.Slice(pos)); + pos += bufferSize; + } + + // 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; + + // Write "XMP DataXMP" + ReadOnlySpan idBytes = GifConstants.XmpApplicationIdentificationBytes; + idBytes.CopyTo(buffer.Slice(bytesWritten)); + bytesWritten += idBytes.Length; + + // XMP Data itself + this.Data.CopyTo(buffer.Slice(bytesWritten)); + bytesWritten += this.Data.Length; + + // Write the Magic Trailer + buffer[bytesWritten++] = 0x01; + for (byte i = 255; i > 0; i--) + { + buffer[bytesWritten++] = i; + } + + buffer[bytesWritten++] = 0x00; + + return totalSize; + } + } +} diff --git a/src/ImageSharp/Formats/Gif/Sections/IGifExtension.cs b/src/ImageSharp/Formats/Gif/Sections/IGifExtension.cs index 5a15a6dfa9..d2783fc48d 100644 --- a/src/ImageSharp/Formats/Gif/Sections/IGifExtension.cs +++ b/src/ImageSharp/Formats/Gif/Sections/IGifExtension.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. using System; @@ -15,6 +15,11 @@ namespace SixLabors.ImageSharp.Formats.Gif /// byte Label { get; } + /// + /// Gets the length of the contents of this extension. + /// + int ContentLength { get; } + /// /// Writes the extension data to the buffer. /// diff --git a/src/ImageSharp/Formats/ImageExtensions.Save.cs b/src/ImageSharp/Formats/ImageExtensions.Save.cs index 55fd00cf95..b6321ddbfe 100644 --- a/src/ImageSharp/Formats/ImageExtensions.Save.cs +++ b/src/ImageSharp/Formats/ImageExtensions.Save.cs @@ -106,7 +106,6 @@ namespace SixLabors.ImageSharp /// The stream to save the image to. /// The encoder to save the image with. /// Thrown if the stream is null. - /// A representing the asynchronous operation. public static void SaveAsBmp(this Image source, Stream stream, BmpEncoder encoder) => source.Save( stream, @@ -209,7 +208,6 @@ namespace SixLabors.ImageSharp /// The stream to save the image to. /// The encoder to save the image with. /// Thrown if the stream is null. - /// A representing the asynchronous operation. public static void SaveAsGif(this Image source, Stream stream, GifEncoder encoder) => source.Save( stream, @@ -312,7 +310,6 @@ namespace SixLabors.ImageSharp /// The stream to save the image to. /// The encoder to save the image with. /// Thrown if the stream is null. - /// A representing the asynchronous operation. public static void SaveAsJpeg(this Image source, Stream stream, JpegEncoder encoder) => source.Save( stream, @@ -415,7 +412,6 @@ namespace SixLabors.ImageSharp /// The stream to save the image to. /// The encoder to save the image with. /// Thrown if the stream is null. - /// A representing the asynchronous operation. public static void SaveAsPbm(this Image source, Stream stream, PbmEncoder encoder) => source.Save( stream, @@ -518,7 +514,6 @@ namespace SixLabors.ImageSharp /// The stream to save the image to. /// The encoder to save the image with. /// Thrown if the stream is null. - /// A representing the asynchronous operation. public static void SaveAsPng(this Image source, Stream stream, PngEncoder encoder) => source.Save( stream, @@ -621,7 +616,6 @@ namespace SixLabors.ImageSharp /// The stream to save the image to. /// The encoder to save the image with. /// Thrown if the stream is null. - /// A representing the asynchronous operation. public static void SaveAsTga(this Image source, Stream stream, TgaEncoder encoder) => source.Save( stream, @@ -724,7 +718,6 @@ namespace SixLabors.ImageSharp /// The stream to save the image to. /// The encoder to save the image with. /// Thrown if the stream is null. - /// A representing the asynchronous operation. public static void SaveAsWebp(this Image source, Stream stream, WebpEncoder encoder) => source.Save( stream, @@ -827,7 +820,6 @@ namespace SixLabors.ImageSharp /// The stream to save the image to. /// The encoder to save the image with. /// Thrown if the stream is null. - /// A representing the asynchronous operation. public static void SaveAsTiff(this Image source, Stream stream, TiffEncoder encoder) => source.Save( stream, diff --git a/src/ImageSharp/Formats/ImageExtensions.Save.tt b/src/ImageSharp/Formats/ImageExtensions.Save.tt index c4a00b37cb..ae7648522f 100644 --- a/src/ImageSharp/Formats/ImageExtensions.Save.tt +++ b/src/ImageSharp/Formats/ImageExtensions.Save.tt @@ -124,7 +124,6 @@ namespace SixLabors.ImageSharp /// The stream to save the image to. /// The encoder to save the image with. /// Thrown if the stream is null. - /// A representing the asynchronous operation. public static void SaveAs<#= fmt #>(this Image source, Stream stream, <#= fmt #>Encoder encoder) => source.Save( stream, diff --git a/src/ImageSharp/Formats/ImageFormatManager.cs b/src/ImageSharp/Formats/ImageFormatManager.cs index 2e18745925..f3fde403da 100644 --- a/src/ImageSharp/Formats/ImageFormatManager.cs +++ b/src/ImageSharp/Formats/ImageFormatManager.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. using System; @@ -17,27 +17,27 @@ namespace SixLabors.ImageSharp.Formats /// Used for locking against as there is no ConcurrentSet type. /// /// - private static readonly object HashLock = new object(); + private static readonly object HashLock = new(); /// /// The list of supported keyed to mime types. /// - private readonly ConcurrentDictionary mimeTypeEncoders = new ConcurrentDictionary(); + private readonly ConcurrentDictionary mimeTypeEncoders = new(); /// /// The list of supported keyed to mime types. /// - private readonly ConcurrentDictionary mimeTypeDecoders = new ConcurrentDictionary(); + private readonly ConcurrentDictionary mimeTypeDecoders = new(); /// /// The list of supported s. /// - private readonly HashSet imageFormats = new HashSet(); + private readonly HashSet imageFormats = new(); /// /// The list of supported s. /// - private ConcurrentBag imageFormatDetectors = new ConcurrentBag(); + private ConcurrentBag imageFormatDetectors = new(); /// /// Initializes a new instance of the class. @@ -113,9 +113,7 @@ namespace SixLabors.ImageSharp.Formats /// The mime-type to discover /// The if found; otherwise null public IImageFormat FindFormatByMimeType(string mimeType) - { - return this.imageFormats.FirstOrDefault(x => x.MimeTypes.Contains(mimeType, StringComparer.OrdinalIgnoreCase)); - } + => this.imageFormats.FirstOrDefault(x => x.MimeTypes.Contains(mimeType, StringComparer.OrdinalIgnoreCase)); /// /// Sets a specific image encoder as the encoder for a specific image format. @@ -146,10 +144,7 @@ namespace SixLabors.ImageSharp.Formats /// /// Removes all the registered image format detectors. /// - public void ClearImageFormatDetectors() - { - this.imageFormatDetectors = new ConcurrentBag(); - } + public void ClearImageFormatDetectors() => this.imageFormatDetectors = new(); /// /// Adds a new detector for detecting mime types. @@ -193,9 +188,6 @@ namespace SixLabors.ImageSharp.Formats /// /// Sets the max header size. /// - private void SetMaxHeaderSize() - { - this.MaxHeaderSize = this.imageFormatDetectors.Max(x => x.HeaderSize); - } + private void SetMaxHeaderSize() => this.MaxHeaderSize = this.imageFormatDetectors.Max(x => x.HeaderSize); } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs index f252864476..d7511fddac 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs @@ -280,7 +280,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components } /// - /// Quantize input block, apply zig-zag ordering and store result as 16bit integers. + /// Quantize input block, transpose, apply zig-zag ordering and store as . /// /// Source block. /// Destination block. @@ -291,19 +291,19 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components if (Avx2.IsSupported) { MultiplyIntoInt16_Avx2(ref block, ref qt, ref dest); - ZigZag.ApplyZigZagOrderingAvx2(ref dest); + ZigZag.ApplyTransposingZigZagOrderingAvx2(ref dest); } else if (Ssse3.IsSupported) { MultiplyIntoInt16_Sse2(ref block, ref qt, ref dest); - ZigZag.ApplyZigZagOrderingSsse3(ref dest); + ZigZag.ApplyTransposingZigZagOrderingSsse3(ref dest); } else #endif { for (int i = 0; i < Size; i++) { - int idx = ZigZag.ZigZagOrder[i]; + int idx = ZigZag.TransposingOrder[i]; float quantizedVal = block[idx] * qt[idx]; quantizedVal += quantizedVal < 0 ? -0.5f : 0.5f; dest[i] = (short)quantizedVal; diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs index ce5e5110b6..e1faf93f49 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs @@ -31,7 +31,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// /// Number of component in the current scan. /// - private int componentsCount; + private int scanComponentCount; /// /// The reset interval determined by RST markers. @@ -112,11 +112,12 @@ 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); @@ -148,11 +149,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,24 +167,11 @@ 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; - // Pre-derive the huffman table to avoid in-loop checks. - for (int i = 0; i < this.componentsCount; i++) - { - int order = this.frame.ComponentOrder[i]; - JpegComponent component = this.components[order]; - - ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DCHuffmanTableId]; - ref HuffmanTable acHuffmanTable = ref this.acHuffmanTables[component.ACHuffmanTableId]; - dcHuffmanTable.Configure(); - acHuffmanTable.Configure(); - } - for (int j = 0; j < mcusPerColumn; j++) { this.cancellationToken.ThrowIfCancellationRequested(); @@ -188,7 +181,7 @@ 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]; @@ -210,7 +203,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; @@ -233,7 +226,7 @@ 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(); } } @@ -248,8 +241,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DCHuffmanTableId]; ref HuffmanTable acHuffmanTable = ref this.acHuffmanTables[component.ACHuffmanTableId]; - dcHuffmanTable.Configure(); - acHuffmanTable.Configure(); for (int j = 0; j < h; j++) { @@ -275,6 +266,52 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder } } + private void ParseBaselineDataSingleComponent() + { + JpegComponent component = this.frame.Components[0]; + int mcuLines = this.frame.McusPerColumn; + int w = component.WidthInBlocks; + int h = component.SamplingFactors.Height; + ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DCHuffmanTableId]; + ref HuffmanTable acHuffmanTable = ref this.acHuffmanTables[component.ACHuffmanTableId]; + + ref HuffmanScanBuffer 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. @@ -297,7 +334,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; } @@ -329,7 +366,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { this.CheckProgressiveData(); - if (this.componentsCount == 1) + if (this.scanComponentCount == 1) { this.ParseProgressiveDataNonInterleaved(); } @@ -347,15 +384,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder int mcusPerLine = this.frame.McusPerLine; ref HuffmanScanBuffer buffer = ref this.scanBuffer; - // Pre-derive the huffman table to avoid in-loop checks. - for (int k = 0; k < this.componentsCount; k++) - { - int order = this.frame.ComponentOrder[k]; - JpegComponent component = this.components[order]; - ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DCHuffmanTableId]; - dcHuffmanTable.Configure(); - } - for (int j = 0; j < mcusPerColumn; j++) { for (int i = 0; i < mcusPerLine; i++) @@ -363,7 +391,7 @@ 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]; @@ -416,7 +444,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder if (this.SpectralStart == 0) { ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DCHuffmanTableId]; - dcHuffmanTable.Configure(); for (int j = 0; j < h; j++) { @@ -444,7 +471,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder else { ref HuffmanTable acHuffmanTable = ref this.acHuffmanTables[component.ACHuffmanTableId]; - acHuffmanTable.Configure(); for (int j = 0; j < h; j++) { @@ -752,11 +778,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// Table index. /// Code lengths. /// Code values. + /// The provided spare workspace memory, can be dirty. [MethodImpl(InliningOptions.ShortMethod)] - public void BuildHuffmanTable(int type, int index, ReadOnlySpan codeLengths, ReadOnlySpan values) + public void BuildHuffmanTable(int type, int index, ReadOnlySpan codeLengths, ReadOnlySpan values, Span workspace) { HuffmanTable[] tables = type == 0 ? this.dcHuffmanTables : this.acHuffmanTables; - tables[index] = new HuffmanTable(codeLengths, values); + tables[index] = new HuffmanTable(codeLengths, values, workspace); } } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs index f18c636278..bee5e0229b 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs @@ -13,12 +13,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder [StructLayout(LayoutKind.Sequential)] internal unsafe struct HuffmanTable { - private bool isConfigured; - /// - /// Derived from the DHT marker. Sizes[k] = # of symbols with codes of length k bits; Sizes[0] is unused. + /// Memory workspace buffer size used in ctor. /// - public fixed byte Sizes[17]; + public const int WorkspaceByteSize = 256 * sizeof(uint); /// /// Derived from the DHT marker. Contains the symbols, in order of incremental code length. @@ -58,51 +56,35 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// /// Initializes a new instance of the struct. /// - /// The code lengths - /// The huffman values - public HuffmanTable(ReadOnlySpan codeLengths, ReadOnlySpan values) + /// The code lengths. + /// The huffman values. + /// The provided spare workspace memory, can be dirty. + public HuffmanTable(ReadOnlySpan codeLengths, ReadOnlySpan values, Span workspace) { - this.isConfigured = false; - Unsafe.CopyBlockUnaligned(ref this.Sizes[0], ref MemoryMarshal.GetReference(codeLengths), (uint)codeLengths.Length); Unsafe.CopyBlockUnaligned(ref this.Values[0], ref MemoryMarshal.GetReference(values), (uint)values.Length); - } - - /// - /// Expands the HuffmanTable into its readable form. - /// - public void Configure() - { - if (this.isConfigured) - { - return; - } - Span huffSize = stackalloc char[257]; - Span huffCode = stackalloc uint[257]; - - // Figure C.1: make table of Huffman code length for each symbol + // Generate codes + uint code = 0; + int si = 1; int p = 0; - for (int j = 1; j <= 16; j++) + for (int i = 1; i <= 16; i++) { - int i = this.Sizes[j]; - while (i-- != 0) + int count = codeLengths[i]; + for (int j = 0; j < count; j++) { - huffSize[p++] = (char)j; + workspace[p++] = code; + code++; } - } - huffSize[p] = (char)0; - - // Figure C.2: generate the codes themselves - uint code = 0; - int si = huffSize[0]; - p = 0; - while (huffSize[p] != 0) - { - while (huffSize[p] == si) + // 'code' is now 1 more than the last code used for codelength 'si' + // in the valid worst possible case 'code' would have the least + // significant bit set to 1, e.g. 1111(0) +1 => 1111(1) + // but it must still fit in 'si' bits since no huffman code can be equal to all 1s + // if last code is all ones, e.g. 1111(1), then incrementing it by 1 would yield + // a new code which occupies one extra bit, e.g. 1111(1) +1 => (1)1111(0) + if (code >= (1 << si)) { - huffCode[p++] = code; - code++; + JpegThrowHelper.ThrowInvalidImageContentException("Bad huffman table."); } code <<= 1; @@ -113,11 +95,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder p = 0; for (int j = 1; j <= 16; j++) { - if (this.Sizes[j] != 0) + if (codeLengths[j] != 0) { - this.ValOffset[j] = p - (int)huffCode[p]; - p += this.Sizes[j]; - this.MaxCode[j] = huffCode[p - 1]; // Maximum code of length l + this.ValOffset[j] = p - (int)workspace[p]; + p += codeLengths[j]; + this.MaxCode[j] = workspace[p - 1]; // Maximum code of length l this.MaxCode[j] <<= JpegConstants.Huffman.RegisterSize - j; // Left justify this.MaxCode[j] |= (1ul << (JpegConstants.Huffman.RegisterSize - j)) - 1; } @@ -142,11 +124,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder for (int length = 1; length <= JpegConstants.Huffman.LookupBits; length++) { int jShift = JpegConstants.Huffman.LookupBits - length; - for (int i = 1; i <= this.Sizes[length]; i++, p++) + for (int i = 1; i <= codeLengths[length]; i++, p++) { // length = current code's length, p = its index in huffCode[] & Values[]. // Generate left-justified code followed by all possible bit sequences - int lookBits = (int)(huffCode[p] << jShift); + int lookBits = (int)(workspace[p] << jShift); for (int ctr = 1 << (JpegConstants.Huffman.LookupBits - length); ctr > 0; ctr--) { this.LookaheadSize[lookBits] = (byte)length; @@ -155,8 +137,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder } } } - - this.isConfigured = true; } } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JFifMarker.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JFifMarker.cs index c7b71f75a8..a95e6c16c2 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JFifMarker.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JFifMarker.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. using System; @@ -103,26 +103,22 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// public bool Equals(JFifMarker other) - { - return this.MajorVersion == other.MajorVersion + => this.MajorVersion == other.MajorVersion && this.MinorVersion == other.MinorVersion && this.DensityUnits == other.DensityUnits && this.XDensity == other.XDensity && this.YDensity == other.YDensity; - } /// public override bool Equals(object obj) => obj is JFifMarker other && this.Equals(other); /// public override int GetHashCode() - { - return HashCode.Combine( + => HashCode.Combine( this.MajorVersion, this.MinorVersion, this.DensityUnits, this.XDensity, this.YDensity); - } } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs index e1e0e160cd..b41c949b26 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs @@ -60,6 +60,18 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder (byte)'E', (byte)'x', (byte)'i', (byte)'f', (byte)'\0', (byte)'\0' }; + /// + /// Gets the XMP specific markers. + /// + public static ReadOnlySpan XmpMarker => new[] + { + (byte)'h', (byte)'t', (byte)'t', (byte)'p', (byte)':', (byte)'/', (byte)'/', + (byte)'n', (byte)'s', (byte)'.', (byte)'a', (byte)'d', (byte)'o', (byte)'b', + (byte)'e', (byte)'.', (byte)'c', (byte)'o', (byte)'m', (byte)'/', (byte)'x', + (byte)'a', (byte)'p', (byte)'/', (byte)'1', (byte)'.', (byte)'0', (byte)'/', + (byte)0 + }; + /// /// Gets the Adobe specific markers . /// diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs index 40411ef288..430adeb21d 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs @@ -111,7 +111,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 diff --git a/src/ImageSharp/Formats/Jpeg/Components/FastFloatingPointDCT.Intrinsic.cs b/src/ImageSharp/Formats/Jpeg/Components/FastFloatingPointDCT.Intrinsic.cs index 94864005ec..8acc4b6269 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/FastFloatingPointDCT.Intrinsic.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/FastFloatingPointDCT.Intrinsic.cs @@ -29,11 +29,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components { DebugGuard.IsTrue(Avx.IsSupported, "Avx support is required to execute this operation."); - // First pass - process rows - block.TransposeInplace(); + // First pass - process columns FDCT8x8_1D_Avx(ref block); - // Second pass - process columns + // Second pass - process rows block.TransposeInplace(); FDCT8x8_1D_Avx(ref block); diff --git a/src/ImageSharp/Formats/Jpeg/Components/FastFloatingPointDCT.cs b/src/ImageSharp/Formats/Jpeg/Components/FastFloatingPointDCT.cs index 81bfe2135d..e1bcff30f3 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/FastFloatingPointDCT.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/FastFloatingPointDCT.cs @@ -92,6 +92,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components tableRef = 0.125f / (tableRef * Unsafe.Add(ref multipliersRef, i)); tableRef = ref Unsafe.Add(ref tableRef, 1); } + + // Spectral macroblocks are not transposed before quantization + // Transpose is done after quantization at zig-zag stage + // so we must transpose quantization table + quantTable.TransposeInplace(); } /// @@ -133,24 +138,21 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components } else #endif - if (Vector.IsHardwareAccelerated) { FDCT_Vector4(ref block); } - else - { - FDCT_Scalar(ref block); - } } /// /// Apply floating point IDCT inplace using API. /// + /// + /// This method can be used even if there's no SIMD intrinsics available + /// as can be compiled to scalar instructions. + /// /// Input block. private static void IDCT_Vector4(ref Block8x8F transposedBlock) { - DebugGuard.IsTrue(Vector.IsHardwareAccelerated, "Scalar implementation should be called for non-accelerated hardware."); - // First pass - process columns IDCT8x4_Vector4(ref transposedBlock.V0L); IDCT8x4_Vector4(ref transposedBlock.V0R); @@ -215,136 +217,17 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components } } - /// - /// Apply 2D floating point FDCT inplace using scalar operations. - /// - /// - /// Ported from libjpeg-turbo https://github.com/libjpeg-turbo/libjpeg-turbo/blob/main/jfdctflt.c. - /// - /// Input block. - private static void FDCT_Scalar(ref Block8x8F block) - { - const int dctSize = 8; - - float tmp0, tmp1, tmp2, tmp3, tmp4, tmp5, tmp6, tmp7; - float tmp10, tmp11, tmp12, tmp13; - float z1, z2, z3, z4, z5, z11, z13; - - // First pass - process rows - ref float blockRef = ref Unsafe.As(ref block); - for (int ctr = 7; ctr >= 0; ctr--) - { - tmp0 = Unsafe.Add(ref blockRef, 0) + Unsafe.Add(ref blockRef, 7); - tmp7 = Unsafe.Add(ref blockRef, 0) - Unsafe.Add(ref blockRef, 7); - tmp1 = Unsafe.Add(ref blockRef, 1) + Unsafe.Add(ref blockRef, 6); - tmp6 = Unsafe.Add(ref blockRef, 1) - Unsafe.Add(ref blockRef, 6); - tmp2 = Unsafe.Add(ref blockRef, 2) + Unsafe.Add(ref blockRef, 5); - tmp5 = Unsafe.Add(ref blockRef, 2) - Unsafe.Add(ref blockRef, 5); - tmp3 = Unsafe.Add(ref blockRef, 3) + Unsafe.Add(ref blockRef, 4); - tmp4 = Unsafe.Add(ref blockRef, 3) - Unsafe.Add(ref blockRef, 4); - - // Even part - tmp10 = tmp0 + tmp3; - tmp13 = tmp0 - tmp3; - tmp11 = tmp1 + tmp2; - tmp12 = tmp1 - tmp2; - - Unsafe.Add(ref blockRef, 0) = tmp10 + tmp11; - Unsafe.Add(ref blockRef, 4) = tmp10 - tmp11; - - z1 = (tmp12 + tmp13) * 0.707106781f; - Unsafe.Add(ref blockRef, 2) = tmp13 + z1; - Unsafe.Add(ref blockRef, 6) = tmp13 - z1; - - // Odd part - tmp10 = tmp4 + tmp5; - tmp11 = tmp5 + tmp6; - tmp12 = tmp6 + tmp7; - - z5 = (tmp10 - tmp12) * 0.382683433f; - z2 = (0.541196100f * tmp10) + z5; - z4 = (1.306562965f * tmp12) + z5; - z3 = tmp11 * 0.707106781f; - - z11 = tmp7 + z3; - z13 = tmp7 - z3; - - Unsafe.Add(ref blockRef, 5) = z13 + z2; - Unsafe.Add(ref blockRef, 3) = z13 - z2; - Unsafe.Add(ref blockRef, 1) = z11 + z4; - Unsafe.Add(ref blockRef, 7) = z11 - z4; - - blockRef = ref Unsafe.Add(ref blockRef, dctSize); - } - - // Second pass - process columns - blockRef = ref Unsafe.As(ref block); - for (int ctr = 7; ctr >= 0; ctr--) - { - tmp0 = Unsafe.Add(ref blockRef, dctSize * 0) + Unsafe.Add(ref blockRef, dctSize * 7); - tmp7 = Unsafe.Add(ref blockRef, dctSize * 0) - Unsafe.Add(ref blockRef, dctSize * 7); - tmp1 = Unsafe.Add(ref blockRef, dctSize * 1) + Unsafe.Add(ref blockRef, dctSize * 6); - tmp6 = Unsafe.Add(ref blockRef, dctSize * 1) - Unsafe.Add(ref blockRef, dctSize * 6); - tmp2 = Unsafe.Add(ref blockRef, dctSize * 2) + Unsafe.Add(ref blockRef, dctSize * 5); - tmp5 = Unsafe.Add(ref blockRef, dctSize * 2) - Unsafe.Add(ref blockRef, dctSize * 5); - tmp3 = Unsafe.Add(ref blockRef, dctSize * 3) + Unsafe.Add(ref blockRef, dctSize * 4); - tmp4 = Unsafe.Add(ref blockRef, dctSize * 3) - Unsafe.Add(ref blockRef, dctSize * 4); - - // Even part - tmp10 = tmp0 + tmp3; - tmp13 = tmp0 - tmp3; - tmp11 = tmp1 + tmp2; - tmp12 = tmp1 - tmp2; - - Unsafe.Add(ref blockRef, dctSize * 0) = tmp10 + tmp11; - Unsafe.Add(ref blockRef, dctSize * 4) = tmp10 - tmp11; - - z1 = (tmp12 + tmp13) * 0.707106781f; - Unsafe.Add(ref blockRef, dctSize * 2) = tmp13 + z1; - Unsafe.Add(ref blockRef, dctSize * 6) = tmp13 - z1; - - // Odd part - tmp10 = tmp4 + tmp5; - tmp11 = tmp5 + tmp6; - tmp12 = tmp6 + tmp7; - - z5 = (tmp10 - tmp12) * 0.382683433f; - z2 = (0.541196100f * tmp10) + z5; - z4 = (1.306562965f * tmp12) + z5; - z3 = tmp11 * 0.707106781f; - - z11 = tmp7 + z3; - z13 = tmp7 - z3; - - Unsafe.Add(ref blockRef, dctSize * 5) = z13 + z2; - Unsafe.Add(ref blockRef, dctSize * 3) = z13 - z2; - Unsafe.Add(ref blockRef, dctSize * 1) = z11 + z4; - Unsafe.Add(ref blockRef, dctSize * 7) = z11 - z4; - - blockRef = ref Unsafe.Add(ref blockRef, 1); - } - } - /// /// Apply floating point FDCT inplace using API. /// - /// - /// This implementation must be called only if hardware supports 4 - /// floating point numbers vector. Otherwise explicit scalar - /// implementation is faster - /// because it does not rely on block transposition. - /// /// Input block. public static void FDCT_Vector4(ref Block8x8F block) { - DebugGuard.IsTrue(Vector.IsHardwareAccelerated, "Scalar implementation should be called for non-accelerated hardware."); - - // First pass - process rows - block.TransposeInplace(); + // First pass - process columns FDCT8x4_Vector4(ref block.V0L); FDCT8x4_Vector4(ref block.V0R); - // Second pass - process columns + // Second pass - process rows block.TransposeInplace(); FDCT8x4_Vector4(ref block.V0L); FDCT8x4_Vector4(ref block.V0R); diff --git a/src/ImageSharp/Formats/Jpeg/Components/ZigZag.Intrinsic.cs b/src/ImageSharp/Formats/Jpeg/Components/ZigZag.Intrinsic.cs index 6577739c1a..850de26c30 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ZigZag.Intrinsic.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ZigZag.Intrinsic.cs @@ -3,6 +3,7 @@ #if SUPPORTS_RUNTIME_INTRINSICS using System; +using System.Runtime.InteropServices; using System.Runtime.Intrinsics; using System.Runtime.Intrinsics.X86; @@ -18,120 +19,138 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components #pragma warning restore SA1309 /// - /// Gets shuffle vectors for + /// Gets shuffle vectors for /// zig zag implementation. /// private static ReadOnlySpan SseShuffleMasks => new byte[] { - // row0 - 0, 1, 2, 3, _, _, _, _, _, _, 4, 5, 6, 7, _, _, - _, _, _, _, 0, 1, _, _, 2, 3, _, _, _, _, 4, 5, - _, _, _, _, _, _, 0, 1, _, _, _, _, _, _, _, _, - - // row1 - _, _, _, _, _, _, _, _, _, _, _, _, 8, 9, 10, 11, - 2, 3, _, _, _, _, _, _, 4, 5, _, _, _, _, _, _, - _, _, 0, 1, _, _, 2, 3, _, _, _, _, _, _, _, _, - - // row2 - _, _, _, _, _, _, 2, 3, _, _, _, _, _, _, 4, 5, - _, _, _, _, _, _, _, _, 0, 1, _, _, 2, 3, _, _, - - // row3 - _, _, _, _, _, _, 12, 13, 14, 15, _, _, _, _, _, _, - _, _, _, _, 10, 11, _, _, _, _, 12, 13, _, _, _, _, - _, _, 8, 9, _, _, _, _, _, _, _, _, 10, 11, _, _, - 6, 7, _, _, _, _, _, _, _, _, _, _, _, _, 8, 9, - - // row4 - _, _, 4, 5, _, _, _, _, _, _, _, _, 6, 7, _, _, - _, _, _, _, 2, 3, _, _, _, _, 4, 5, _, _, _, _, - _, _, _, _, _, _, 0, 1, 2, 3, _, _, _, _, _, _, - - // row5 - _, _, 12, 13, _, _, 14, 15, _, _, _, _, _, _, _, _, - 10, 11, _, _, _, _, _, _, 12, 13, _, _, _, _, _, _, - - // row6 - _, _, _, _, _, _, _, _, 12, 13, _, _, 14, 15, _, _, - _, _, _, _, _, _, 10, 11, _, _, _, _, _, _, 12, 13, - 4, 5, 6, 7, _, _, _, _, _, _, _, _, _, _, _, _, - - // row7 - 10, 11, _, _, _, _, 12, 13, _, _, 14, 15, _, _, _, _, - _, _, 8, 9, 10, 11, _, _, _, _, _, _, 12, 13, 14, 15 +#pragma warning disable SA1515 + /* row0 - A0 B0 A1 A2 B1 C0 D0 C1 */ + // A + 0, 1, _, _, 2, 3, 4, 5, _, _, _, _, _, _, _, _, + // B + _, _, 0, 1, _, _, _, _, 2, 3, _, _, _, _, _, _, + // C + _, _, _, _, _, _, _, _, _, _, 0, 1, _, _, 2, 3, + + /* row1 - B2 A3 A4 B3 C2 D1 E0 F0 */ + // A + _, _, 6, 7, 8, 9, _, _, _, _, _, _, _, _, _, _, + // B + 4, 5, _, _, _, _, 6, 7, _, _, _, _, _, _, _, _, + + /* row2 - E1 D2 C3 B4 A5 A6 B5 C4 */ + // A + _, _, _, _, _, _, _, _, 10, 11, 12, 13, _, _, _, _, + // B + _, _, _, _, _, _, 8, 9, _, _, _, _, 10, 11, _, _, + // C + _, _, _, _, 6, 7, _, _, _, _, _, _, _, _, 8, 9, + + /* row3 - D3 E2 F1 G0 H0 G1 F2 E3 */ + // E + _, _, 4, 5, _, _, _, _, _, _, _, _, _, _, 6, 7, + // F + _, _, _, _, 2, 3, _, _, _, _, _, _, 4, 5, _, _, + // G + _, _, _, _, _, _, 0, 1, _, _, 2, 3, _, _, _, _, + + /* row4 - D4 C5 B6 A7 B7 C6 D5 E4 */ + // B + _, _, _, _, 12, 13, _, _, 14, 15, _, _, _, _, _, _, + // C + _, _, 10, 11, _, _, _, _, _, _, 12, 13, _, _, _, _, + // D + 8, 9, _, _, _, _, _, _, _, _, _, _, 10, 11, _, _, + + /* row5 - F3 G2 H1 H2 G3 F4 E5 D6 */ + // F + 6, 7, _, _, _, _, _, _, _, _, 8, 9, _, _, _, _, + // G + _, _, 4, 5, _, _, _, _, 6, 7, _, _, _, _, _, _, + // H + _, _, _, _, 2, 3, 4, 5, _, _, _, _, _, _, _, _, + + /* row6 - C7 D7 E6 F5 G4 H3 H4 G5 */ + // G + _, _, _, _, _, _, _, _, 8, 9, _, _, _, _, 10, 11, + // H + _, _, _, _, _, _, _, _, _, _, 6, 7, 8, 9, _, _, + + /* row7 - F6 E7 F7 G6 H5 H6 G7 H7 */ + // F + 12, 13, _, _, 14, 15, _, _, _, _, _, _, _, _, _, _, + // G + _, _, _, _, _, _, 12, 13, _, _, _, _, 14, 15, _, _, + // H + _, _, _, _, _, _, _, _, 10, 11, 12, 13, _, _, 14, 15, +#pragma warning restore SA1515 }; /// - /// Gets shuffle vectors for + /// Gets shuffle vectors for /// zig zag implementation. /// private static ReadOnlySpan AvxShuffleMasks => new byte[] { - // 01_AB/01_EF/23_CD - cross-lane - 0, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, - - // 01_AB - inner-lane - 0, 1, 2, 3, 8, 9, _, _, 10, 11, 4, 5, 6, 7, 12, 13, _, _, _, _, _, _, _, _, _, _, 10, 11, 4, 5, 6, 7, - - // 01_CD/23_GH - cross-lane - 0, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, _, _, _, _, 0, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, _, _, _, _, - - // 01_CD - inner-lane - _, _, _, _, _, _, 0, 1, _, _, _, _, _, _, _, _, 2, 3, 8, 9, _, _, 10, 11, 4, 5, _, _, _, _, _, _, - - // 01_EF - inner-lane - _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 0, 1, _, _, _, _, _, _, _, _, _, _, - - // 23_AB/45_CD/67_EF - cross-lane - 3, 0, 0, 0, 6, 0, 0, 0, 7, 0, 0, 0, _, _, _, _, 3, 0, 0, 0, 6, 0, 0, 0, 7, 0, 0, 0, _, _, _, _, - - // 23_AB - inner-lane - 4, 5, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 6, 7, 0, 1, 2, 3, 8, 9, _, _, _, _, - - // 23_CD - inner-lane - _, _, 6, 7, 12, 13, _, _, _, _, _, _, _, _, _, _, 10, 11, 4, 5, _, _, _, _, _, _, _, _, 6, 7, 12, 13, - - // 23_EF - inner-lane - _, _, _, _, _, _, 2, 3, 8, 9, _, _, 10, 11, 4, 5, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, - - // 23_GH - inner-lane - _, _, _, _, _, _, _, _, _, _, 0, 1, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, - - // 45_AB - inner-lane - _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 10, 11, _, _, _, _, _, _, _, _, _, _, - - // 45_CD - inner-lane - _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 6, 7, 0, 1, _, _, 2, 3, 8, 9, _, _, _, _, _, _, - - // 45_EF - cross-lane - 1, 0, 0, 0, 2, 0, 0, 0, 5, 0, 0, 0, _, _, _, _, 2, 0, 0, 0, 3, 0, 0, 0, 6, 0, 0, 0, 7, 0, 0, 0, - - // 45_EF - inner-lane - 2, 3, 8, 9, _, _, _, _, _, _, _, _, 10, 11, 4, 5, _, _, _, _, _, _, _, _, _, _, 2, 3, 8, 9, _, _, - - // 45_GH - inner-lane - _, _, _, _, 2, 3, 8, 9, 10, 11, 4, 5, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 6, 7, - - // 67_CD - inner-lane - _, _, _, _, _, _, _, _, _, _, 10, 11, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, - - // 67_EF - inner-lane - _, _, _, _, _, _, 6, 7, 0, 1, _, _, 2, 3, 8, 9, _, _, _, _, _, _, _, _, 10, 11, _, _, _, _, _, _, - - // 67_GH - inner-lane - 8, 9, 10, 11, 4, 5, _, _, _, _, _, _, _, _, _, _, 2, 3, 8, 9, 10, 11, 4, 5, _, _, 6, 7, 12, 13, 14, 15 +#pragma warning disable SA1515 + /* 01 */ + // [cr] crln_01_AB_CD + 0, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, _, _, _, _, 1, 0, 0, 0, 2, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, + // (in) AB + 0, 1, 8, 9, 2, 3, 4, 5, 10, 11, _, _, _, _, _, _, 12, 13, 2, 3, 4, 5, 14, 15, _, _, _, _, _, _, _, _, + // (in) CD + _, _, _, _, _, _, _, _, _, _, 0, 1, 8, 9, 2, 3, _, _, _, _, _, _, _, _, 0, 1, 10, 11, _, _, _, _, + // [cr] crln_01_23_EF_23_CD + 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, + // (in) EF + _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 0, 1, 8, 9, + + /* 23 */ + // [cr] crln_23_AB_23_45_GH + 2, 0, 0, 0, 3, 0, 0, 0, 6, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, + // (in) AB + _, _, _, _, _, _, 8, 9, 2, 3, 4, 5, 10, 11, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, + // (in) CDe + _, _, 12, 13, 6, 7, _, _, _, _, _, _, _, _, 8, 9, 14, 15, _, _, _, _, _, _, _, _, _, _, _, _, _, _, + // (in) EF + 2, 3, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 4, 5, 10, 11, _, _, _, _, _, _, 12, 13, 6, 7, + // (in) GH + _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 0, 1, 8, 9, 2, 3, _, _, _, _, + + /* 45 */ + // (in) AB + _, _, _, _, 12, 13, 6, 7, 14, 15, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, + // [cr] crln_45_67_CD_45_EF + 2, 0, 0, 0, 3, 0, 0, 0, 6, 0, 0, 0, 7, 0, 0, 0, 2, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 7, 0, 0, 0, + // (in) CD + 8, 9, 2, 3, _, _, _, _, _, _, 4, 5, 10, 11, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 12, 13, + // (in) EF + _, _, _, _, _, _, _, _, _, _, _, _, _, _, 0, 1, 6, 7, _, _, _, _, _, _, _, _, 8, 9, 2, 3, _, _, + // (in) GH + _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 4, 5, 10, 11, 12, 13, 6, 7, _, _, _, _, _, _, + + /* 67 */ + // (in) CD + 6, 7, 14, 15, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, + // [cr] crln_67_EF_67_GH + 2, 0, 0, 0, 3, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 3, 0, 0, 0, 6, 0, 0, 0, 7, 0, 0, 0, _, _, _, _, + // (in) EF + _, _, _, _, 4, 5, 14, 15, _, _, _, _, _, _, _, _, 8, 9, 2, 3, 10, 11, _, _, _, _, _, _, _, _, _, _, + // (in) GH + _, _, _, _, _, _, _, _, 0, 1, 10, 11, 12, 13, 2, 3, _, _, _, _, _, _, 0, 1, 6, 7, 8, 9, 2, 3, 10, 11, +#pragma warning restore SA1515 }; /// /// Applies zig zag ordering for given 8x8 matrix using SSE cpu intrinsics. /// /// Input matrix. - public static unsafe void ApplyZigZagOrderingSsse3(ref Block8x8 block) + public static unsafe void ApplyTransposingZigZagOrderingSsse3(ref Block8x8 block) { DebugGuard.IsTrue(Ssse3.IsSupported, "Ssse3 support is required to run this operation!"); - fixed (byte* maskPtr = SseShuffleMasks) + fixed (byte* shuffleVectorsPtr = &MemoryMarshal.GetReference(SseShuffleMasks)) { Vector128 rowA = block.V0.AsByte(); Vector128 rowB = block.V1.AsByte(); @@ -142,73 +161,69 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components Vector128 rowG = block.V6.AsByte(); Vector128 rowH = block.V7.AsByte(); - // row0 - A0 A1 B0 C0 B1 A2 A3 B2 - Vector128 rowA0 = Ssse3.Shuffle(rowA, Sse2.LoadVector128(maskPtr + (16 * 0))).AsInt16(); - Vector128 rowB0 = Ssse3.Shuffle(rowB, Sse2.LoadVector128(maskPtr + (16 * 1))).AsInt16(); - Vector128 row0 = Sse2.Or(rowA0, rowB0); - Vector128 rowC0 = Ssse3.Shuffle(rowC, Sse2.LoadVector128(maskPtr + (16 * 2))).AsInt16(); - row0 = Sse2.Or(row0, rowC0); - - // row1 - C1 D0 E0 D1 C2 B3 A4 A5 - Vector128 rowA1 = Ssse3.Shuffle(rowA, Sse2.LoadVector128(maskPtr + (16 * 3))).AsInt16(); - Vector128 rowC1 = Ssse3.Shuffle(rowC, Sse2.LoadVector128(maskPtr + (16 * 4))).AsInt16(); - Vector128 row1 = Sse2.Or(rowA1, rowC1); - Vector128 rowD1 = Ssse3.Shuffle(rowD, Sse2.LoadVector128(maskPtr + (16 * 5))).AsInt16(); - row1 = Sse2.Or(row1, rowD1); - row1 = Sse2.Insert(row1.AsUInt16(), Sse2.Extract(rowB.AsUInt16(), 3), 5).AsInt16(); - row1 = Sse2.Insert(row1.AsUInt16(), Sse2.Extract(rowE.AsUInt16(), 0), 2).AsInt16(); - - // row2 - Vector128 rowE2 = Ssse3.Shuffle(rowE, Sse2.LoadVector128(maskPtr + (16 * 6))).AsInt16(); - Vector128 rowF2 = Ssse3.Shuffle(rowF, Sse2.LoadVector128(maskPtr + (16 * 7))).AsInt16(); - Vector128 row2 = Sse2.Or(rowE2, rowF2); - row2 = Sse2.Insert(row2.AsUInt16(), Sse2.Extract(rowB.AsUInt16(), 4), 0).AsInt16(); - row2 = Sse2.Insert(row2.AsUInt16(), Sse2.Extract(rowC.AsUInt16(), 3), 1).AsInt16(); - row2 = Sse2.Insert(row2.AsUInt16(), Sse2.Extract(rowD.AsUInt16(), 2), 2).AsInt16(); - row2 = Sse2.Insert(row2.AsUInt16(), Sse2.Extract(rowG.AsUInt16(), 0), 5).AsInt16(); - - // row3 - Vector128 rowA3 = Ssse3.Shuffle(rowA, Sse2.LoadVector128(maskPtr + (16 * 8))).AsInt16().AsInt16(); - Vector128 rowB3 = Ssse3.Shuffle(rowB, Sse2.LoadVector128(maskPtr + (16 * 9))).AsInt16().AsInt16(); - Vector128 row3 = Sse2.Or(rowA3, rowB3); - Vector128 rowC3 = Ssse3.Shuffle(rowC, Sse2.LoadVector128(maskPtr + (16 * 10))).AsInt16(); - row3 = Sse2.Or(row3, rowC3); - Vector128 shuffleRowD3EF = Sse2.LoadVector128(maskPtr + (16 * 11)); - Vector128 rowD3 = Ssse3.Shuffle(rowD, shuffleRowD3EF).AsInt16(); - row3 = Sse2.Or(row3, rowD3); - - // row4 - Vector128 rowE4 = Ssse3.Shuffle(rowE, shuffleRowD3EF).AsInt16(); - Vector128 rowF4 = Ssse3.Shuffle(rowF, Sse2.LoadVector128(maskPtr + (16 * 12))).AsInt16(); - Vector128 row4 = Sse2.Or(rowE4, rowF4); - Vector128 rowG4 = Ssse3.Shuffle(rowG, Sse2.LoadVector128(maskPtr + (16 * 13))).AsInt16(); - row4 = Sse2.Or(row4, rowG4); - Vector128 rowH4 = Ssse3.Shuffle(rowH, Sse2.LoadVector128(maskPtr + (16 * 14))).AsInt16(); - row4 = Sse2.Or(row4, rowH4); - - // row5 - Vector128 rowC5 = Ssse3.Shuffle(rowC, Sse2.LoadVector128(maskPtr + (16 * 15))).AsInt16(); - Vector128 rowD5 = Ssse3.Shuffle(rowD, Sse2.LoadVector128(maskPtr + (16 * 16))).AsInt16(); - Vector128 row5 = Sse2.Or(rowC5, rowD5); - row5 = Sse2.Insert(row5.AsUInt16(), Sse2.Extract(rowB.AsUInt16(), 7), 2).AsInt16(); - row5 = Sse2.Insert(row5.AsUInt16(), Sse2.Extract(rowE.AsUInt16(), 5), 5).AsInt16(); - row5 = Sse2.Insert(row5.AsUInt16(), Sse2.Extract(rowF.AsUInt16(), 4), 6).AsInt16(); - row5 = Sse2.Insert(row5.AsUInt16(), Sse2.Extract(rowG.AsUInt16(), 3), 7).AsInt16(); - - // row6 - Vector128 rowE6 = Ssse3.Shuffle(rowE, Sse2.LoadVector128(maskPtr + (16 * 17))).AsInt16(); - Vector128 rowF6 = Ssse3.Shuffle(rowF, Sse2.LoadVector128(maskPtr + (16 * 18))).AsInt16(); - Vector128 row6 = Sse2.Or(rowE6, rowF6); - Vector128 rowH6 = Ssse3.Shuffle(rowH, Sse2.LoadVector128(maskPtr + (16 * 19))).AsInt16(); - row6 = Sse2.Or(row6, rowH6); - row6 = Sse2.Insert(row6.AsUInt16(), Sse2.Extract(rowD.AsUInt16(), 7), 5).AsInt16(); - row6 = Sse2.Insert(row6.AsUInt16(), Sse2.Extract(rowG.AsUInt16(), 4), 2).AsInt16(); - - // row7 - Vector128 rowG7 = Ssse3.Shuffle(rowG, Sse2.LoadVector128(maskPtr + (16 * 20))).AsInt16(); - Vector128 rowH7 = Ssse3.Shuffle(rowH, Sse2.LoadVector128(maskPtr + (16 * 21))).AsInt16(); - Vector128 row7 = Sse2.Or(rowG7, rowH7); - row7 = Sse2.Insert(row7.AsUInt16(), Sse2.Extract(rowF.AsUInt16(), 7), 4).AsInt16(); + // row0 - A0 B0 A1 A2 B1 C0 D0 C1 + Vector128 row0_A = Ssse3.Shuffle(rowA, Sse2.LoadVector128(shuffleVectorsPtr + (16 * 0))).AsInt16(); + Vector128 row0_B = Ssse3.Shuffle(rowB, Sse2.LoadVector128(shuffleVectorsPtr + (16 * 1))).AsInt16(); + Vector128 row0_C = Ssse3.Shuffle(rowC, Sse2.LoadVector128(shuffleVectorsPtr + (16 * 2))).AsInt16(); + Vector128 row0 = Sse2.Or(Sse2.Or(row0_A, row0_B), row0_C); + row0 = Sse2.Insert(row0.AsUInt16(), Sse2.Extract(rowD.AsUInt16(), 0), 6).AsInt16(); + + // row1 - B2 A3 A4 B3 C2 D1 E0 F0 + Vector128 row1_A = Ssse3.Shuffle(rowA, Sse2.LoadVector128(shuffleVectorsPtr + (16 * 3))).AsInt16(); + Vector128 row1_B = Ssse3.Shuffle(rowB, Sse2.LoadVector128(shuffleVectorsPtr + (16 * 4))).AsInt16(); + Vector128 row1 = Sse2.Or(row1_A, row1_B); + row1 = Sse2.Insert(row1.AsUInt16(), Sse2.Extract(rowC.AsUInt16(), 2), 4).AsInt16(); + row1 = Sse2.Insert(row1.AsUInt16(), Sse2.Extract(rowD.AsUInt16(), 1), 5).AsInt16(); + row1 = Sse2.Insert(row1.AsUInt16(), Sse2.Extract(rowE.AsUInt16(), 0), 6).AsInt16(); + row1 = Sse2.Insert(row1.AsUInt16(), Sse2.Extract(rowF.AsUInt16(), 0), 7).AsInt16(); + + // row2 - E1 D2 C3 B4 A5 A6 B5 C4 + Vector128 row2_A = Ssse3.Shuffle(rowA, Sse2.LoadVector128(shuffleVectorsPtr + (16 * 5))).AsInt16(); + Vector128 row2_B = Ssse3.Shuffle(rowB, Sse2.LoadVector128(shuffleVectorsPtr + (16 * 6))).AsInt16(); + Vector128 row2_C = Ssse3.Shuffle(rowC, Sse2.LoadVector128(shuffleVectorsPtr + (16 * 7))).AsInt16(); + Vector128 row2 = Sse2.Or(Sse2.Or(row2_A, row2_B), row2_C); + row2 = Sse2.Insert(row2.AsUInt16(), Sse2.Extract(rowD.AsUInt16(), 2), 1).AsInt16(); + row2 = Sse2.Insert(row2.AsUInt16(), Sse2.Extract(rowE.AsUInt16(), 1), 0).AsInt16(); + + // row3 - D3 E2 F1 G0 H0 G1 F2 E3 + Vector128 row3_E = Ssse3.Shuffle(rowE, Sse2.LoadVector128(shuffleVectorsPtr + (16 * 8))).AsInt16(); + Vector128 row3_F = Ssse3.Shuffle(rowF, Sse2.LoadVector128(shuffleVectorsPtr + (16 * 9))).AsInt16(); + Vector128 row3_G = Ssse3.Shuffle(rowG, Sse2.LoadVector128(shuffleVectorsPtr + (16 * 10))).AsInt16(); + Vector128 row3 = Sse2.Or(Sse2.Or(row3_E, row3_F), row3_G); + row3 = Sse2.Insert(row3.AsUInt16(), Sse2.Extract(rowD.AsUInt16(), 3), 0).AsInt16(); + row3 = Sse2.Insert(row3.AsUInt16(), Sse2.Extract(rowH.AsUInt16(), 0), 4).AsInt16(); + + // row4 - D4 C5 B6 A7 B7 C6 D5 E4 + Vector128 row4_B = Ssse3.Shuffle(rowB, Sse2.LoadVector128(shuffleVectorsPtr + (16 * 11))).AsInt16(); + Vector128 row4_C = Ssse3.Shuffle(rowC, Sse2.LoadVector128(shuffleVectorsPtr + (16 * 12))).AsInt16(); + Vector128 row4_D = Ssse3.Shuffle(rowD, Sse2.LoadVector128(shuffleVectorsPtr + (16 * 13))).AsInt16(); + Vector128 row4 = Sse2.Or(Sse2.Or(row4_B, row4_C), row4_D); + row4 = Sse2.Insert(row4.AsUInt16(), Sse2.Extract(rowA.AsUInt16(), 7), 3).AsInt16(); + row4 = Sse2.Insert(row4.AsUInt16(), Sse2.Extract(rowE.AsUInt16(), 4), 7).AsInt16(); + + // row5 - F3 G2 H1 H2 G3 F4 E5 D6 + Vector128 row5_F = Ssse3.Shuffle(rowF, Sse2.LoadVector128(shuffleVectorsPtr + (16 * 14))).AsInt16(); + Vector128 row5_G = Ssse3.Shuffle(rowG, Sse2.LoadVector128(shuffleVectorsPtr + (16 * 15))).AsInt16(); + Vector128 row5_H = Ssse3.Shuffle(rowH, Sse2.LoadVector128(shuffleVectorsPtr + (16 * 16))).AsInt16(); + Vector128 row5 = Sse2.Or(Sse2.Or(row5_F, row5_G), row5_H); + row5 = Sse2.Insert(row5.AsUInt16(), Sse2.Extract(rowD.AsUInt16(), 6), 7).AsInt16(); + row5 = Sse2.Insert(row5.AsUInt16(), Sse2.Extract(rowE.AsUInt16(), 5), 6).AsInt16(); + + // row6 - C7 D7 E6 F5 G4 H3 H4 G5 + Vector128 row6_G = Ssse3.Shuffle(rowG, Sse2.LoadVector128(shuffleVectorsPtr + (16 * 17))).AsInt16(); + Vector128 row6_H = Ssse3.Shuffle(rowH, Sse2.LoadVector128(shuffleVectorsPtr + (16 * 18))).AsInt16(); + Vector128 row6 = Sse2.Or(row6_G, row6_H); + row6 = Sse2.Insert(row6.AsUInt16(), Sse2.Extract(rowC.AsUInt16(), 7), 0).AsInt16(); + row6 = Sse2.Insert(row6.AsUInt16(), Sse2.Extract(rowD.AsUInt16(), 7), 1).AsInt16(); + row6 = Sse2.Insert(row6.AsUInt16(), Sse2.Extract(rowE.AsUInt16(), 6), 2).AsInt16(); + row6 = Sse2.Insert(row6.AsUInt16(), Sse2.Extract(rowF.AsUInt16(), 5), 3).AsInt16(); + + // row7 - F6 E7 F7 G6 H5 H6 G7 H7 + Vector128 row7_F = Ssse3.Shuffle(rowF, Sse2.LoadVector128(shuffleVectorsPtr + (16 * 19))).AsInt16(); + Vector128 row7_G = Ssse3.Shuffle(rowG, Sse2.LoadVector128(shuffleVectorsPtr + (16 * 20))).AsInt16(); + Vector128 row7_H = Ssse3.Shuffle(rowH, Sse2.LoadVector128(shuffleVectorsPtr + (16 * 21))).AsInt16(); + Vector128 row7 = Sse2.Or(Sse2.Or(row7_F, row7_G), row7_H); + row7 = Sse2.Insert(row7.AsUInt16(), Sse2.Extract(rowE.AsUInt16(), 7), 1).AsInt16(); block.V0 = row0; block.V1 = row1; @@ -225,69 +240,61 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// Applies zig zag ordering for given 8x8 matrix using AVX cpu intrinsics. /// /// Input matrix. - public static unsafe void ApplyZigZagOrderingAvx2(ref Block8x8 block) + public static unsafe void ApplyTransposingZigZagOrderingAvx2(ref Block8x8 block) { DebugGuard.IsTrue(Avx2.IsSupported, "Avx2 support is required to run this operation!"); - fixed (byte* shuffleVectorsPtr = AvxShuffleMasks) + fixed (byte* shuffleVectorsPtr = &MemoryMarshal.GetReference(AvxShuffleMasks)) { - Vector256 rowsAB = block.V01.AsByte(); - Vector256 rowsCD = block.V23.AsByte(); - Vector256 rowsEF = block.V45.AsByte(); - Vector256 rowsGH = block.V67.AsByte(); - - // rows 0 1 - Vector256 rows_AB01_EF01_CD23_shuffleMask = Avx.LoadVector256(shuffleVectorsPtr + (0 * 32)).AsInt32(); - Vector256 row01_AB = Avx2.PermuteVar8x32(rowsAB.AsInt32(), rows_AB01_EF01_CD23_shuffleMask).AsByte(); + Vector256 rowAB = block.V01.AsByte(); + Vector256 rowCD = block.V23.AsByte(); + Vector256 rowEF = block.V45.AsByte(); + Vector256 rowGH = block.V67.AsByte(); + + /* row01 - A0 B0 A1 A2 B1 C0 D0 C1 | B2 A3 A4 B3 C2 D1 E0 F0 */ + Vector256 crln_01_AB_CD = Avx.LoadVector256(shuffleVectorsPtr + (0 * 32)).AsInt32(); + Vector256 row01_AB = Avx2.PermuteVar8x32(rowAB.AsInt32(), crln_01_AB_CD).AsByte(); row01_AB = Avx2.Shuffle(row01_AB, Avx.LoadVector256(shuffleVectorsPtr + (1 * 32))).AsByte(); - - Vector256 rows_CD01_GH23_shuffleMask = Avx.LoadVector256(shuffleVectorsPtr + (2 * 32)).AsInt32(); - Vector256 row01_CD = Avx2.PermuteVar8x32(rowsCD.AsInt32(), rows_CD01_GH23_shuffleMask).AsByte(); - row01_CD = Avx2.Shuffle(row01_CD, Avx.LoadVector256(shuffleVectorsPtr + (3 * 32))).AsByte(); - - Vector256 row0123_EF = Avx2.PermuteVar8x32(rowsEF.AsInt32(), rows_AB01_EF01_CD23_shuffleMask).AsByte(); - Vector256 row01_EF = Avx2.Shuffle(row0123_EF, Avx.LoadVector256(shuffleVectorsPtr + (4 * 32))).AsByte(); - - Vector256 row01 = Avx2.Or(Avx2.Or(row01_AB, row01_CD), row01_EF); - - // rows 2 3 - Vector256 rows_AB23_CD45_EF67_shuffleMask = Avx.LoadVector256(shuffleVectorsPtr + (5 * 32)).AsInt32(); - Vector256 row2345_AB = Avx2.PermuteVar8x32(rowsAB.AsInt32(), rows_AB23_CD45_EF67_shuffleMask).AsByte(); - Vector256 row23_AB = Avx2.Shuffle(row2345_AB, Avx.LoadVector256(shuffleVectorsPtr + (6 * 32))).AsByte(); - - Vector256 row23_CD = Avx2.PermuteVar8x32(rowsCD.AsInt32(), rows_AB01_EF01_CD23_shuffleMask).AsByte(); + Vector256 row01_CD = Avx2.PermuteVar8x32(rowCD.AsInt32(), crln_01_AB_CD).AsByte(); + row01_CD = Avx2.Shuffle(row01_CD, Avx.LoadVector256(shuffleVectorsPtr + (2 * 32))).AsByte(); + Vector256 crln_01_23_EF_23_CD = Avx.LoadVector256(shuffleVectorsPtr + (3 * 32)).AsInt32(); + Vector256 row01_23_EF = Avx2.PermuteVar8x32(rowEF.AsInt32(), crln_01_23_EF_23_CD).AsByte(); + Vector256 row01_EF = Avx2.Shuffle(row01_23_EF, Avx.LoadVector256(shuffleVectorsPtr + (4 * 32))).AsByte(); + + Vector256 row01 = Avx2.Or(row01_AB, Avx2.Or(row01_CD, row01_EF)); + + /* row23 - E1 D2 C3 B4 A5 A6 B5 C4 | D3 E2 F1 G0 H0 G1 F2 E3 */ + Vector256 crln_23_AB_23_45_GH = Avx.LoadVector256(shuffleVectorsPtr + (5 * 32)).AsInt32(); + Vector256 row23_45_AB = Avx2.PermuteVar8x32(rowAB.AsInt32(), crln_23_AB_23_45_GH).AsByte(); + Vector256 row23_AB = Avx2.Shuffle(row23_45_AB, Avx.LoadVector256(shuffleVectorsPtr + (6 * 32))).AsByte(); + Vector256 row23_CD = Avx2.PermuteVar8x32(rowCD.AsInt32(), crln_01_23_EF_23_CD).AsByte(); row23_CD = Avx2.Shuffle(row23_CD, Avx.LoadVector256(shuffleVectorsPtr + (7 * 32))).AsByte(); - - Vector256 row23_EF = Avx2.Shuffle(row0123_EF, Avx.LoadVector256(shuffleVectorsPtr + (8 * 32))).AsByte(); - - Vector256 row2345_GH = Avx2.PermuteVar8x32(rowsGH.AsInt32(), rows_CD01_GH23_shuffleMask).AsByte(); - Vector256 row23_GH = Avx2.Shuffle(row2345_GH, Avx.LoadVector256(shuffleVectorsPtr + (9 * 32)).AsByte()); + Vector256 row23_EF = Avx2.Shuffle(row01_23_EF, Avx.LoadVector256(shuffleVectorsPtr + (8 * 32))).AsByte(); + Vector256 row23_45_GH = Avx2.PermuteVar8x32(rowGH.AsInt32(), crln_23_AB_23_45_GH).AsByte(); + Vector256 row23_GH = Avx2.Shuffle(row23_45_GH, Avx.LoadVector256(shuffleVectorsPtr + (9 * 32))).AsByte(); Vector256 row23 = Avx2.Or(Avx2.Or(row23_AB, row23_CD), Avx2.Or(row23_EF, row23_GH)); - // rows 4 5 - Vector256 row45_AB = Avx2.Shuffle(row2345_AB, Avx.LoadVector256(shuffleVectorsPtr + (10 * 32)).AsByte()); - Vector256 row4567_CD = Avx2.PermuteVar8x32(rowsCD.AsInt32(), rows_AB23_CD45_EF67_shuffleMask).AsByte(); - Vector256 row45_CD = Avx2.Shuffle(row4567_CD, Avx.LoadVector256(shuffleVectorsPtr + (11 * 32)).AsByte()); - - Vector256 rows_EF45_GH67_shuffleMask = Avx.LoadVector256(shuffleVectorsPtr + (12 * 32)).AsInt32(); - Vector256 row45_EF = Avx2.PermuteVar8x32(rowsEF.AsInt32(), rows_EF45_GH67_shuffleMask).AsByte(); - row45_EF = Avx2.Shuffle(row45_EF, Avx.LoadVector256(shuffleVectorsPtr + (13 * 32)).AsByte()); - - Vector256 row45_GH = Avx2.Shuffle(row2345_GH, Avx.LoadVector256(shuffleVectorsPtr + (14 * 32)).AsByte()); + /* row45 - D4 C5 B6 A7 B7 C6 D5 E4 | F3 G2 H1 H2 G3 F4 E5 D6 */ + Vector256 row45_AB = Avx2.Shuffle(row23_45_AB, Avx.LoadVector256(shuffleVectorsPtr + (10 * 32))).AsByte(); + Vector256 crln_45_67_CD_45_EF = Avx.LoadVector256(shuffleVectorsPtr + (11 * 32)).AsInt32(); + Vector256 row45_67_CD = Avx2.PermuteVar8x32(rowCD.AsInt32(), crln_45_67_CD_45_EF).AsByte(); + Vector256 row45_CD = Avx2.Shuffle(row45_67_CD, Avx.LoadVector256(shuffleVectorsPtr + (12 * 32))).AsByte(); + Vector256 row45_EF = Avx2.PermuteVar8x32(rowEF.AsInt32(), crln_45_67_CD_45_EF).AsByte(); + row45_EF = Avx2.Shuffle(row45_EF, Avx.LoadVector256(shuffleVectorsPtr + (13 * 32))).AsByte(); + Vector256 row45_GH = Avx2.Shuffle(row23_45_GH, Avx.LoadVector256(shuffleVectorsPtr + (14 * 32))).AsByte(); Vector256 row45 = Avx2.Or(Avx2.Or(row45_AB, row45_CD), Avx2.Or(row45_EF, row45_GH)); - // rows 6 7 - Vector256 row67_CD = Avx2.Shuffle(row4567_CD, Avx.LoadVector256(shuffleVectorsPtr + (15 * 32)).AsByte()); - - Vector256 row67_EF = Avx2.PermuteVar8x32(rowsEF.AsInt32(), rows_AB23_CD45_EF67_shuffleMask).AsByte(); - row67_EF = Avx2.Shuffle(row67_EF, Avx.LoadVector256(shuffleVectorsPtr + (16 * 32)).AsByte()); - - Vector256 row67_GH = Avx2.PermuteVar8x32(rowsGH.AsInt32(), rows_EF45_GH67_shuffleMask).AsByte(); - row67_GH = Avx2.Shuffle(row67_GH, Avx.LoadVector256(shuffleVectorsPtr + (17 * 32)).AsByte()); + /* row67 - C7 D7 E6 F5 G4 H3 H4 G5 | F6 E7 F7 G6 H5 H6 G7 H7 */ + Vector256 row67_CD = Avx2.Shuffle(row45_67_CD, Avx.LoadVector256(shuffleVectorsPtr + (15 * 32))).AsByte(); + Vector256 crln_67_EF_67_GH = Avx.LoadVector256(shuffleVectorsPtr + (16 * 32)).AsInt32(); + Vector256 row67_EF = Avx2.PermuteVar8x32(rowEF.AsInt32(), crln_67_EF_67_GH).AsByte(); + row67_EF = Avx2.Shuffle(row67_EF, Avx.LoadVector256(shuffleVectorsPtr + (17 * 32))).AsByte(); + Vector256 row67_GH = Avx2.PermuteVar8x32(rowGH.AsInt32(), crln_67_EF_67_GH).AsByte(); + row67_GH = Avx2.Shuffle(row67_GH, Avx.LoadVector256(shuffleVectorsPtr + (18 * 32))).AsByte(); - Vector256 row67 = Avx2.Or(Avx2.Or(row67_CD, row67_EF), row67_GH); + Vector256 row67 = Avx2.Or(row67_CD, Avx2.Or(row67_EF, row67_GH)); block.V01 = row01.AsInt16(); block.V23 = row23.AsInt16(); diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 4b68c39ea9..023928f37d 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -17,6 +17,7 @@ using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Iptc; +using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Jpeg @@ -46,7 +47,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// Whether the image has an EXIF marker. /// - private bool isExif; + private bool hasExif; /// /// Contains exif data. @@ -56,7 +57,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// Whether the image has an ICC marker. /// - private bool isIcc; + private bool hasIcc; /// /// Contains ICC data. @@ -66,13 +67,23 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// Whether the image has a IPTC data. /// - private bool isIptc; + private bool hasIptc; /// /// Contains IPTC data. /// private byte[] iptcData; + /// + /// Whether the image has a XMP data. + /// + private bool hasXmp; + + /// + /// Contains XMP data. + /// + private byte[] xmpData; + /// /// Contains information about the JFIF marker. /// @@ -183,6 +194,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg this.InitExifProfile(); this.InitIccProfile(); this.InitIptcProfile(); + this.InitXmpProfile(); this.InitDerivedMetadataProperties(); return new Image( @@ -198,6 +210,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg this.InitExifProfile(); this.InitIccProfile(); this.InitIptcProfile(); + this.InitXmpProfile(); this.InitDerivedMetadataProperties(); Size pixelSize = this.Frame.PixelSize; @@ -472,7 +485,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg : JpegColorSpace.Cmyk; } - JpegThrowHelper.ThrowInvalidImageContentException($"Unsupported color mode. Supported component counts 1, 3, and 4; found {componentCount}"); + JpegThrowHelper.ThrowNotSupportedComponentCount(componentCount); return default; } @@ -539,7 +552,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// private void InitExifProfile() { - if (this.isExif) + if (this.hasExif) { this.Metadata.ExifProfile = new ExifProfile(this.exifData); } @@ -550,7 +563,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// private void InitIccProfile() { - if (this.isIcc) + if (this.hasIcc) { var profile = new IccProfile(this.iccData); if (profile.CheckIsValid()) @@ -565,13 +578,25 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// private void InitIptcProfile() { - if (this.isIptc) + if (this.hasIptc) { var profile = new IptcProfile(this.iptcData); this.Metadata.IptcProfile = profile; } } + /// + /// Initializes the XMP profile. + /// + private void InitXmpProfile() + { + if (this.hasXmp) + { + var profile = new XmpProfile(this.xmpData); + this.Metadata.XmpProfile = profile; + } + } + /// /// Assigns derived metadata properties to , eg. horizontal and vertical resolution if it has a JFIF header. /// @@ -583,7 +608,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg this.Metadata.VerticalResolution = this.jFif.YDensity; this.Metadata.ResolutionUnits = this.jFif.DensityUnits; } - else if (this.isExif) + else if (this.hasExif) { double horizontalValue = this.GetExifResolutionValue(ExifTag.XResolution); double verticalValue = this.GetExifResolutionValue(ExifTag.YResolution); @@ -625,7 +650,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg private void ProcessApplicationHeaderMarker(BufferedReadStream stream, int remaining) { // We can only decode JFif identifiers. - if (remaining < JFifMarker.Length) + // Some images contain multiple JFIF markers (Issue 1932) so we check to see + // if it's already been read. + if (remaining < JFifMarker.Length || (!this.jFif.Equals(default))) { // Skip the application header length stream.Skip(remaining); @@ -656,8 +683,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// The remaining bytes in the segment block. private void ProcessApp1Marker(BufferedReadStream stream, int remaining) { - const int Exif00 = 6; - if (remaining < Exif00 || this.IgnoreMetadata) + const int ExifMarkerLength = 6; + const int XmpMarkerLength = 29; + if (remaining < ExifMarkerLength || this.IgnoreMetadata) { // Skip the application header length stream.Skip(remaining); @@ -669,23 +697,55 @@ namespace SixLabors.ImageSharp.Formats.Jpeg JpegThrowHelper.ThrowInvalidImageContentException("Bad App1 Marker length."); } - byte[] profile = new byte[remaining]; - stream.Read(profile, 0, remaining); + // XMP marker is the longest, so read at least that many bytes into temp. + stream.Read(this.temp, 0, ExifMarkerLength); - if (ProfileResolver.IsProfile(profile, ProfileResolver.ExifMarker)) + if (ProfileResolver.IsProfile(this.temp, ProfileResolver.ExifMarker)) { - this.isExif = true; + remaining -= ExifMarkerLength; + this.hasExif = true; + byte[] profile = new byte[remaining]; + stream.Read(profile, 0, remaining); + if (this.exifData is null) { - // The first 6 bytes (Exif00) will be skipped, because this is Jpeg specific - this.exifData = profile.AsSpan(Exif00).ToArray(); + this.exifData = profile; } else { // If the EXIF information exceeds 64K, it will be split over multiple APP1 markers - this.ExtendProfile(ref this.exifData, profile.AsSpan(Exif00).ToArray()); + this.ExtendProfile(ref this.exifData, profile); } + + remaining = 0; } + + 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))) + { + this.hasXmp = true; + byte[] profile = new byte[remaining]; + stream.Read(profile, 0, remaining); + + if (this.xmpData is null) + { + this.xmpData = profile; + } + else + { + // If the XMP information exceeds 64K, it will be split over multiple APP1 markers + this.ExtendProfile(ref this.xmpData, profile); + } + + remaining = 0; + } + } + + // Skip over any remaining bytes of this header. + stream.Skip(remaining); } /// @@ -709,7 +769,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg if (ProfileResolver.IsProfile(identifier, ProfileResolver.IccMarker)) { - this.isIcc = true; + this.hasIcc = true; byte[] profile = new byte[remaining]; stream.Read(profile, 0, remaining); @@ -768,7 +828,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg int dataStartIdx = 2 + resourceBlockNameLength + 4; if (resourceDataSize > 0 && blockDataSpan.Length >= dataStartIdx + resourceDataSize) { - this.isIptc = true; + this.hasIptc = true; this.iptcData = blockDataSpan.Slice(dataStartIdx, resourceDataSize).ToArray(); break; } @@ -788,6 +848,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } } } + else + { + // If the profile is unknown skip over the rest of it. + stream.Skip(remaining); + } } /// @@ -998,6 +1063,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg // 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 + if (componentCount > 4) + { + JpegThrowHelper.ThrowNotSupportedComponentCount(componentCount); + } + this.Frame = new JpegFrame(frameMarker, precision, frameWidth, frameHeight, componentCount); remaining -= length; @@ -1087,12 +1160,18 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// The remaining bytes in the segment block. private void ProcessDefineHuffmanTablesMarker(BufferedReadStream stream, int remaining) { - int length = remaining; + const int codeLengthsByteSize = 17; + const int codeValuesMaxByteSize = 256; + const int totalBufferSize = codeLengthsByteSize + codeValuesMaxByteSize + HuffmanTable.WorkspaceByteSize; - using (IMemoryOwner huffmanData = this.Configuration.MemoryAllocator.Allocate(256, AllocationOptions.Clean)) + int length = remaining; + using (IMemoryOwner buffer = this.Configuration.MemoryAllocator.Allocate(totalBufferSize)) { - Span huffmanDataSpan = huffmanData.GetSpan(); - ref byte huffmanDataRef = ref MemoryMarshal.GetReference(huffmanDataSpan); + Span bufferSpan = buffer.GetSpan(); + Span huffmanLegthsSpan = bufferSpan.Slice(0, codeLengthsByteSize); + Span huffmanValuesSpan = bufferSpan.Slice(codeLengthsByteSize, codeValuesMaxByteSize); + Span tableWorkspace = MemoryMarshal.Cast(bufferSpan.Slice(codeLengthsByteSize + codeValuesMaxByteSize)); + for (int i = 2; i < remaining;) { byte huffmanTableSpec = (byte)stream.ReadByte(); @@ -1102,49 +1181,40 @@ namespace SixLabors.ImageSharp.Formats.Jpeg // Types 0..1 DC..AC if (tableType > 1) { - JpegThrowHelper.ThrowInvalidImageContentException($"Bad huffman table type: {tableType}"); + JpegThrowHelper.ThrowInvalidImageContentException($"Bad huffman table type: {tableType}."); } // Max tables of each type if (tableIndex > 3) { - JpegThrowHelper.ThrowInvalidImageContentException($"Bad huffman table index: {tableIndex}"); + JpegThrowHelper.ThrowInvalidImageContentException($"Bad huffman table index: {tableIndex}."); } - stream.Read(huffmanDataSpan, 0, 16); + stream.Read(huffmanLegthsSpan, 1, 16); - using (IMemoryOwner codeLengths = this.Configuration.MemoryAllocator.Allocate(17, AllocationOptions.Clean)) + int codeLengthSum = 0; + for (int j = 1; j < 17; j++) { - Span codeLengthsSpan = codeLengths.GetSpan(); - ref byte codeLengthsRef = ref MemoryMarshal.GetReference(codeLengthsSpan); - int codeLengthSum = 0; - - for (int j = 1; j < 17; j++) - { - codeLengthSum += Unsafe.Add(ref codeLengthsRef, j) = Unsafe.Add(ref huffmanDataRef, j - 1); - } + codeLengthSum += huffmanLegthsSpan[j]; + } - length -= 17; + length -= 17; - if (codeLengthSum > 256 || codeLengthSum > length) - { - JpegThrowHelper.ThrowInvalidImageContentException("Huffman table has excessive length."); - } + if (codeLengthSum > 256 || codeLengthSum > length) + { + JpegThrowHelper.ThrowInvalidImageContentException("Huffman table has excessive length."); + } - using (IMemoryOwner huffmanValues = this.Configuration.MemoryAllocator.Allocate(256, AllocationOptions.Clean)) - { - Span huffmanValuesSpan = huffmanValues.GetSpan(); - stream.Read(huffmanValuesSpan, 0, codeLengthSum); + stream.Read(huffmanValuesSpan, 0, codeLengthSum); - i += 17 + codeLengthSum; + i += 17 + codeLengthSum; - this.scanDecoder.BuildHuffmanTable( - tableType, - tableIndex, - codeLengthsSpan, - huffmanValuesSpan); - } - } + this.scanDecoder.BuildHuffmanTable( + tableType, + tableIndex, + huffmanLegthsSpan, + huffmanValuesSpan.Slice(0, codeLengthSum), + tableWorkspace); } } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index abe59516fa..a3cff8f31d 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -14,6 +14,7 @@ using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Iptc; +using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Jpeg @@ -109,7 +110,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg this.WriteJfifApplicationHeader(metadata); } - // Write Exif, ICC and IPTC profiles + // Write Exif, XMP, ICC and IPTC profiles this.WriteProfiles(metadata); if (this.colorType == JpegColorType.Rgb) @@ -466,6 +467,54 @@ namespace SixLabors.ImageSharp.Formats.Jpeg this.outputStream.Write(data, 0, data.Length); } + /// + /// Writes the XMP metadata. + /// + /// The XMP metadata to write. + /// + /// Thrown if the XMP profile size exceeds the limit of 65533 bytes. + /// + private void WriteXmpProfile(XmpProfile xmpProfile) + { + if (xmpProfile is null) + { + return; + } + + const int XmpOverheadLength = 29; + const int Max = 65533; + const int MaxData = Max - XmpOverheadLength; + + byte[] data = xmpProfile.Data; + + if (data is null || data.Length == 0) + { + return; + } + + int dataLength = data.Length; + int offset = 0; + + while (dataLength > 0) + { + int length = dataLength; // Number of bytes to write. + + if (length > MaxData) + { + length = MaxData; + } + + dataLength -= length; + + int app1Length = 2 + ProfileResolver.XmpMarker.Length + length; + this.WriteApp1Header(app1Length); + this.outputStream.Write(ProfileResolver.XmpMarker); + this.outputStream.Write(data, offset, length); + + offset += length; + } + } + /// /// Writes the App1 header. /// @@ -579,8 +628,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg return; } + // For compatibility, place the profiles in the following order: + // - APP1 EXIF + // - APP1 XMP + // - APP2 ICC + // - APP13 IPTC metadata.SyncProfiles(); this.WriteExifProfile(metadata.ExifProfile); + this.WriteXmpProfile(metadata.XmpProfile); this.WriteIccProfile(metadata.IccProfile); this.WriteIptcProfile(metadata.IptcProfile); } diff --git a/src/ImageSharp/Formats/Jpeg/JpegThrowHelper.cs b/src/ImageSharp/Formats/Jpeg/JpegThrowHelper.cs index 0292fbcab4..b238e45ef3 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegThrowHelper.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegThrowHelper.cs @@ -45,5 +45,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg [MethodImpl(InliningOptions.ColdPath)] public static void ThrowDimensionsTooLarge(int width, int height) => throw new ImageFormatException($"Image is too large to encode at {width}x{height} for JPEG format."); + + [MethodImpl(InliningOptions.ColdPath)] + public static void ThrowNotSupportedComponentCount(int componentCount) => throw new NotSupportedException($"Images with {componentCount} components are not supported."); } } diff --git a/src/ImageSharp/Formats/PixelTypeInfo.cs b/src/ImageSharp/Formats/PixelTypeInfo.cs index 718b05e33a..afa3b19a22 100644 --- a/src/ImageSharp/Formats/PixelTypeInfo.cs +++ b/src/ImageSharp/Formats/PixelTypeInfo.cs @@ -15,8 +15,17 @@ namespace SixLabors.ImageSharp.Formats /// Initializes a new instance of the class. /// /// Color depth, in number of bits per pixel. - /// Tthe pixel alpha transparency behavior. - internal PixelTypeInfo(int bitsPerPixel, PixelAlphaRepresentation? alpha = null) + public PixelTypeInfo(int bitsPerPixel) + { + this.BitsPerPixel = bitsPerPixel; + } + + /// + /// Initializes a new instance of the class. + /// + /// Color depth, in number of bits per pixel. + /// The pixel alpha transparency behavior. + public PixelTypeInfo(int bitsPerPixel, PixelAlphaRepresentation alpha) { this.BitsPerPixel = bitsPerPixel; this.AlphaRepresentation = alpha; diff --git a/src/ImageSharp/Formats/Png/PngConstants.cs b/src/ImageSharp/Formats/Png/PngConstants.cs index b4ef28083e..fcc8fd992c 100644 --- a/src/ImageSharp/Formats/Png/PngConstants.cs +++ b/src/ImageSharp/Formats/Png/PngConstants.cs @@ -78,5 +78,29 @@ namespace SixLabors.ImageSharp.Formats.Png 0x1A, // EOF 0x0A // LF }; + + /// + /// Gets the keyword of the XMP metadata, encoded in an iTXT chunk. + /// + public static ReadOnlySpan XmpKeyword => new byte[] + { + (byte)'X', + (byte)'M', + (byte)'L', + (byte)':', + (byte)'c', + (byte)'o', + (byte)'m', + (byte)'.', + (byte)'a', + (byte)'d', + (byte)'o', + (byte)'b', + (byte)'e', + (byte)'.', + (byte)'x', + (byte)'m', + (byte)'p' + }; } } diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index f81cbca211..f5fc86ee4d 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -11,6 +11,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; using System.Threading; +using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Compression.Zlib; using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.Formats.Png.Filters; @@ -18,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.Xmp; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Png @@ -187,20 +189,20 @@ namespace SixLabors.ImageSharp.Formats.Png this.AssignTransparentMarkers(alpha, pngMetadata); break; case PngChunkType.Text: - this.ReadTextChunk(pngMetadata, chunk.Data.GetSpan()); + this.ReadTextChunk(metadata, pngMetadata, chunk.Data.GetSpan()); break; case PngChunkType.CompressedText: - this.ReadCompressedTextChunk(pngMetadata, chunk.Data.GetSpan()); + this.ReadCompressedTextChunk(metadata, pngMetadata, chunk.Data.GetSpan()); break; case PngChunkType.InternationalText: - this.ReadInternationalTextChunk(pngMetadata, chunk.Data.GetSpan()); + this.ReadInternationalTextChunk(metadata, chunk.Data.GetSpan()); break; case PngChunkType.Exif: if (!this.ignoreMetadata) { byte[] exifData = new byte[chunk.Length]; chunk.Data.GetSpan().CopyTo(exifData); - metadata.ExifProfile = new ExifProfile(exifData); + this.MergeOrSetExifProfile(metadata, new ExifProfile(exifData), replaceExistingKeys: true); } break; @@ -297,7 +299,7 @@ namespace SixLabors.ImageSharp.Formats.Png break; } - this.ReadTextChunk(pngMetadata, chunk.Data.GetSpan()); + this.ReadTextChunk(metadata, pngMetadata, chunk.Data.GetSpan()); break; case PngChunkType.CompressedText: if (this.colorMetadataOnly) @@ -306,7 +308,7 @@ namespace SixLabors.ImageSharp.Formats.Png break; } - this.ReadCompressedTextChunk(pngMetadata, chunk.Data.GetSpan()); + this.ReadCompressedTextChunk(metadata, pngMetadata, chunk.Data.GetSpan()); break; case PngChunkType.InternationalText: if (this.colorMetadataOnly) @@ -315,7 +317,7 @@ namespace SixLabors.ImageSharp.Formats.Png break; } - this.ReadInternationalTextChunk(pngMetadata, chunk.Data.GetSpan()); + this.ReadInternationalTextChunk(metadata, chunk.Data.GetSpan()); break; case PngChunkType.Exif: if (this.colorMetadataOnly) @@ -328,7 +330,7 @@ namespace SixLabors.ImageSharp.Formats.Png { byte[] exifData = new byte[chunk.Length]; chunk.Data.GetSpan().CopyTo(exifData); - metadata.ExifProfile = new ExifProfile(exifData); + this.MergeOrSetExifProfile(metadata, new ExifProfile(exifData), replaceExistingKeys: true); } break; @@ -967,9 +969,10 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// Reads a text chunk containing image properties from the data. /// + /// The object. /// The metadata to decode to. /// The containing the data. - private void ReadTextChunk(PngMetadata metadata, ReadOnlySpan data) + private void ReadTextChunk(ImageMetadata baseMetadata, PngMetadata metadata, ReadOnlySpan data) { if (this.ignoreMetadata) { @@ -992,15 +995,19 @@ namespace SixLabors.ImageSharp.Formats.Png string value = PngConstants.Encoding.GetString(data.Slice(zeroIndex + 1)); - metadata.TextData.Add(new PngTextData(name, value, string.Empty, string.Empty)); + if (!this.TryReadTextChunkMetadata(baseMetadata, name, value)) + { + metadata.TextData.Add(new PngTextData(name, value, string.Empty, string.Empty)); + } } /// /// Reads the compressed text chunk. Contains a uncompressed keyword and a compressed text string. /// + /// The object. /// The metadata to decode to. /// The containing the data. - private void ReadCompressedTextChunk(PngMetadata metadata, ReadOnlySpan data) + private void ReadCompressedTextChunk(ImageMetadata baseMetadata, PngMetadata metadata, ReadOnlySpan data) { if (this.ignoreMetadata) { @@ -1028,12 +1035,185 @@ namespace SixLabors.ImageSharp.Formats.Png ReadOnlySpan compressedData = data.Slice(zeroIndex + 2); - if (this.TryUncompressTextData(compressedData, PngConstants.Encoding, out string uncompressed)) + if (this.TryUncompressTextData(compressedData, PngConstants.Encoding, out string uncompressed) && + !this.TryReadTextChunkMetadata(baseMetadata, name, uncompressed)) { metadata.TextData.Add(new PngTextData(name, uncompressed, string.Empty, string.Empty)); } } + /// + /// Checks if the given text chunk is actually storing parsable metadata. + /// + /// The object to store the parsed metadata in. + /// The name of the text chunk. + /// The contents of the text chunk. + /// True if metadata was successfully parsed from the text chunk. False if the + /// text chunk was not identified as metadata, and should be stored in the metadata + /// object unmodified. + private bool TryReadTextChunkMetadata(ImageMetadata baseMetadata, string chunkName, string chunkText) + { + if (chunkName.Equals("Raw profile type exif", StringComparison.OrdinalIgnoreCase) && + this.TryReadLegacyExifTextChunk(baseMetadata, chunkText)) + { + // Successfully parsed legacy exif data from text + return true; + } + + // TODO: "Raw profile type iptc", potentially others? + + // No special chunk data identified + return false; + } + + /// + /// Reads exif data encoded into a text chunk with the name "raw profile type exif". + /// This method was used by ImageMagick, exiftool, exiv2, digiKam, etc, before the + /// 2017 update to png that allowed a true exif chunk. + /// + /// The to store the decoded exif tags into. + /// The contents of the "raw profile type exif" text chunk. + private bool TryReadLegacyExifTextChunk(ImageMetadata metadata, string data) + { + ReadOnlySpan dataSpan = data.AsSpan(); + dataSpan = dataSpan.TrimStart(); + + if (!StringEqualsInsensitive(dataSpan.Slice(0, 4), "exif".AsSpan())) + { + // "exif" identifier is missing from the beginning of the text chunk + return false; + } + + // Skip to the data length + dataSpan = dataSpan.Slice(4).TrimStart(); + int dataLengthEnd = dataSpan.IndexOf('\n'); + int dataLength = ParseInt32(dataSpan.Slice(0, dataSpan.IndexOf('\n'))); + + // Skip to the hex-encoded data + dataSpan = dataSpan.Slice(dataLengthEnd).Trim(); + + // Sequence of bytes for the exif header ("Exif" ASCII and two zero bytes). + // This doesn't actually allocate. + ReadOnlySpan exifHeader = new byte[] { 0x45, 0x78, 0x69, 0x66, 0x00, 0x00 }; + + if (dataLength < exifHeader.Length) + { + // Not enough room for the required exif header, this data couldn't possibly be valid + return false; + } + + // Parse the hex-encoded data into the byte array we are going to hand off to ExifProfile + byte[] exifBlob = new byte[dataLength - exifHeader.Length]; + + try + { + // Check for the presence of the exif header in the hex-encoded binary data + byte[] tempExifBuf = exifBlob; + if (exifBlob.Length < exifHeader.Length) + { + // Need to allocate a temporary array, this should be an extremely uncommon (TODO: impossible?) case + tempExifBuf = new byte[exifHeader.Length]; + } + + HexConverter.HexStringToBytes(dataSpan.Slice(0, exifHeader.Length * 2), tempExifBuf); + if (!tempExifBuf.AsSpan().Slice(0, exifHeader.Length).SequenceEqual(exifHeader)) + { + // Exif header in the hex data is not valid + return false; + } + + // Skip over the exif header we just tested + dataSpan = dataSpan.Slice(exifHeader.Length * 2); + dataLength -= exifHeader.Length; + + // Load the hex-encoded data, one line at a time + for (int i = 0; i < dataLength;) + { + ReadOnlySpan lineSpan = dataSpan; + + int newlineIndex = dataSpan.IndexOf('\n'); + if (newlineIndex != -1) + { + lineSpan = dataSpan.Slice(0, newlineIndex); + } + + i += HexConverter.HexStringToBytes(lineSpan, exifBlob.AsSpan().Slice(i)); + + dataSpan = dataSpan.Slice(newlineIndex + 1); + } + } + catch + { + return false; + } + + this.MergeOrSetExifProfile(metadata, new ExifProfile(exifBlob), replaceExistingKeys: false); + return true; + } + + /// + /// Compares two ReadOnlySpan<char>s in a case-insensitive method. + /// This is only needed because older frameworks are missing the extension method. + /// + /// The first to compare. + /// The second to compare. + /// True if the spans were identical, false otherwise. + private static bool StringEqualsInsensitive(ReadOnlySpan span1, ReadOnlySpan span2) + { +#pragma warning disable IDE0022 // Use expression body for methods +#if NETSTANDARD2_1 || NETCOREAPP2_1_OR_GREATER + return span1.Equals(span2, StringComparison.OrdinalIgnoreCase); +#else + return span1.ToString().Equals(span2.ToString(), StringComparison.OrdinalIgnoreCase); +#endif +#pragma warning restore IDE0022 // Use expression body for methods + } + + /// + /// int.Parse() a ReadOnlySpan<char>, with a fallback for older frameworks. + /// + /// The to parse. + /// The parsed . + private static int ParseInt32(ReadOnlySpan span) + { +#pragma warning disable IDE0022 // Use expression body for methods +#if NETSTANDARD2_1 || NETCOREAPP2_1_OR_GREATER + return int.Parse(span); +#else + return int.Parse(span.ToString()); +#endif +#pragma warning restore IDE0022 // Use expression body for methods + } + + /// + /// Sets the in to , + /// or copies exif tags if already contains an . + /// + /// The to store the exif data in. + /// The to copy exif tags from. + /// If already contains an , + /// controls whether existing exif tags in will be overwritten with any conflicting + /// tags from . + private void MergeOrSetExifProfile(ImageMetadata metadata, ExifProfile newProfile, bool replaceExistingKeys) + { + if (metadata.ExifProfile is null) + { + // No exif metadata was loaded yet, so just assign it + metadata.ExifProfile = newProfile; + } + else + { + // Try to merge existing keys with the ones from the new profile + foreach (IExifValue newKey in newProfile.Values) + { + if (replaceExistingKeys || metadata.ExifProfile.GetValueInternal(newKey.Tag) is null) + { + metadata.ExifProfile.SetValueInternal(newKey.Tag, newKey.GetValue()); + } + } + } + } + /// /// Reads a iTXt chunk, which contains international text data. It contains: /// - A uncompressed keyword. @@ -1045,13 +1225,14 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// The metadata to decode to. /// The containing the data. - private void ReadInternationalTextChunk(PngMetadata metadata, ReadOnlySpan data) + private void ReadInternationalTextChunk(ImageMetadata metadata, ReadOnlySpan data) { if (this.ignoreMetadata) { return; } + PngMetadata pngMetadata = metadata.GetPngMetadata(); int zeroIndexKeyword = data.IndexOf((byte)0); if (zeroIndexKeyword < PngConstants.MinTextKeywordLength || zeroIndexKeyword > PngConstants.MaxTextKeywordLength) { @@ -1097,13 +1278,18 @@ namespace SixLabors.ImageSharp.Formats.Png if (this.TryUncompressTextData(compressedData, PngConstants.TranslatedEncoding, out string uncompressed)) { - metadata.TextData.Add(new PngTextData(keyword, uncompressed, language, translatedKeyword)); + pngMetadata.TextData.Add(new PngTextData(keyword, uncompressed, language, translatedKeyword)); } } + else if (this.IsXmpTextData(keywordBytes)) + { + XmpProfile xmpProfile = new XmpProfile(data.Slice(dataStartIdx).ToArray()); + metadata.XmpProfile = xmpProfile; + } else { string value = PngConstants.TranslatedEncoding.GetString(data.Slice(dataStartIdx)); - metadata.TextData.Add(new PngTextData(keyword, value, language, translatedKeyword)); + pngMetadata.TextData.Add(new PngTextData(keyword, value, language, translatedKeyword)); } } @@ -1371,6 +1557,8 @@ namespace SixLabors.ImageSharp.Formats.Png return true; } + private bool IsXmpTextData(ReadOnlySpan keywordBytes) => keywordBytes.SequenceEqual(PngConstants.XmpKeyword); + private void SwapScanlineBuffers() { IMemoryOwner temp = this.previousScanline; diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 5e067aba57..c443c0fcf1 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -138,6 +138,7 @@ namespace SixLabors.ImageSharp.Formats.Png this.WriteTransparencyChunk(stream, pngMetadata); this.WritePhysicalChunk(stream, metadata); this.WriteExifChunk(stream, metadata); + this.WriteXmpChunk(stream, metadata); this.WriteTextChunks(stream, pngMetadata); this.WriteDataChunks(clearTransparency ? clonedImage : image, quantized, stream); this.WriteEndChunk(stream); @@ -654,6 +655,51 @@ namespace SixLabors.ImageSharp.Formats.Png this.WriteChunk(stream, PngChunkType.Exif, meta.ExifProfile.ToByteArray()); } + /// + /// Writes an iTXT chunk, containing the XMP metdata to the stream, if such profile is present in the metadata. + /// + /// The containing image data. + /// The image metadata. + private void WriteXmpChunk(Stream stream, ImageMetadata meta) + { + const int iTxtHeaderSize = 5; + if (((this.options.ChunkFilter ?? PngChunkFilter.None) & PngChunkFilter.ExcludeTextChunks) == PngChunkFilter.ExcludeTextChunks) + { + return; + } + + if (meta.XmpProfile is null) + { + return; + } + + var xmpData = meta.XmpProfile.Data; + + if (xmpData.Length == 0) + { + return; + } + + int payloadLength = xmpData.Length + PngConstants.XmpKeyword.Length + iTxtHeaderSize; + using (IMemoryOwner owner = this.memoryAllocator.Allocate(payloadLength)) + { + Span payload = owner.GetSpan(); + 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; + + // And the XMP data itself + xmpData.CopyTo(payload.Slice(bytesWritten)); + this.WriteChunk(stream, PngChunkType.InternationalText, payload); + } + } + /// /// 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. @@ -693,21 +739,33 @@ namespace SixLabors.ImageSharp.Formats.Png byte[] translatedKeyword = PngConstants.TranslatedEncoding.GetBytes(textData.TranslatedKeyword); byte[] languageTag = PngConstants.LanguageEncoding.GetBytes(textData.LanguageTag); - Span outputBytes = new byte[keywordBytes.Length + textBytes.Length + - translatedKeyword.Length + languageTag.Length + 5]; - keywordBytes.CopyTo(outputBytes); - if (textData.Value.Length > this.options.TextCompressionThreshold) + int payloadLength = keywordBytes.Length + textBytes.Length + translatedKeyword.Length + languageTag.Length + 5; + using (IMemoryOwner owner = this.memoryAllocator.Allocate(payloadLength)) { - // Indicate that the text is compressed. - outputBytes[keywordBytes.Length + 1] = 1; - } + Span outputBytes = owner.GetSpan(); + keywordBytes.CopyTo(outputBytes); + int bytesWritten = keywordBytes.Length; + outputBytes[bytesWritten++] = 0; + if (textData.Value.Length > this.options.TextCompressionThreshold) + { + // Indicate that the text is compressed. + outputBytes[bytesWritten++] = 1; + } + else + { + outputBytes[bytesWritten++] = 0; + } - int keywordStart = keywordBytes.Length + 3; - languageTag.CopyTo(outputBytes.Slice(keywordStart)); - int translatedKeywordStart = keywordStart + languageTag.Length + 1; - translatedKeyword.CopyTo(outputBytes.Slice(translatedKeywordStart)); - textBytes.CopyTo(outputBytes.Slice(translatedKeywordStart + translatedKeyword.Length + 1)); - this.WriteChunk(stream, PngChunkType.InternationalText, outputBytes.ToArray()); + outputBytes[bytesWritten++] = 0; + languageTag.CopyTo(outputBytes.Slice(bytesWritten)); + bytesWritten += languageTag.Length; + outputBytes[bytesWritten++] = 0; + translatedKeyword.CopyTo(outputBytes.Slice(bytesWritten)); + bytesWritten += translatedKeyword.Length; + outputBytes[bytesWritten++] = 0; + textBytes.CopyTo(outputBytes.Slice(bytesWritten)); + this.WriteChunk(stream, PngChunkType.InternationalText, outputBytes); + } } else { @@ -716,19 +774,32 @@ namespace SixLabors.ImageSharp.Formats.Png // Write zTXt chunk. byte[] compressedData = this.GetCompressedTextBytes(PngConstants.Encoding.GetBytes(textData.Value)); - Span outputBytes = new byte[textData.Keyword.Length + compressedData.Length + 2]; - PngConstants.Encoding.GetBytes(textData.Keyword).CopyTo(outputBytes); - compressedData.CopyTo(outputBytes.Slice(textData.Keyword.Length + 2)); - this.WriteChunk(stream, PngChunkType.CompressedText, outputBytes.ToArray()); + 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; + compressedData.CopyTo(outputBytes.Slice(bytesWritten)); + this.WriteChunk(stream, PngChunkType.CompressedText, outputBytes.ToArray()); + } } else { // Write tEXt chunk. - Span outputBytes = new byte[textData.Keyword.Length + textData.Value.Length + 1]; - PngConstants.Encoding.GetBytes(textData.Keyword).CopyTo(outputBytes); - PngConstants.Encoding.GetBytes(textData.Value) - .CopyTo(outputBytes.Slice(textData.Keyword.Length + 1)); - this.WriteChunk(stream, PngChunkType.Text, outputBytes.ToArray()); + int payloadLength = textData.Keyword.Length + textData.Value.Length + 1; + 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; + PngConstants.Encoding.GetBytes(textData.Value) + .CopyTo(outputBytes.Slice(bytesWritten)); + this.WriteChunk(stream, PngChunkType.Text, outputBytes.ToArray()); + } } } } diff --git a/src/ImageSharp/Formats/Tiff/Compression/Compressors/T4BitCompressor.cs b/src/ImageSharp/Formats/Tiff/Compression/Compressors/T4BitCompressor.cs index 30da537eb2..d038e9c8bf 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 0000000000..9e03d2764a --- /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 0000000000..3166106216 --- /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/TiffBaseDecompressor.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffBaseDecompressor.cs index d183a33bdd..986d585d30 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/TiffBaseDecompressor.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/TiffBaseDecompressor.cs @@ -34,17 +34,15 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression /// The number of bytes to read from the input stream. /// The height of the strip. /// The output buffer for uncompressed data. - public void Decompress(BufferedReadStream stream, uint stripOffset, uint stripByteCount, int stripHeight, Span buffer) + public void Decompress(BufferedReadStream stream, ulong stripOffset, ulong stripByteCount, int stripHeight, Span buffer) { - if (stripByteCount > int.MaxValue) - { - TiffThrowHelper.ThrowImageFormatException("The StripByteCount value is too big."); - } + DebugGuard.MustBeLessThanOrEqualTo(stripOffset, (ulong)long.MaxValue, nameof(stripOffset)); + DebugGuard.MustBeLessThanOrEqualTo(stripByteCount, (ulong)long.MaxValue, nameof(stripByteCount)); - stream.Seek(stripOffset, SeekOrigin.Begin); + stream.Seek((long)stripOffset, SeekOrigin.Begin); this.Decompress(stream, (int)stripByteCount, stripHeight, buffer); - if (stripOffset + stripByteCount < stream.Position) + if ((long)stripOffset + (long)stripByteCount < stream.Position) { TiffThrowHelper.ThrowImageFormatException("Out of range when reading a strip."); } diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffCompressorFactory.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffCompressorFactory.cs index db2b935b74..904470e81d 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 031494fc5d..0baf4c89c5 100644 --- a/src/ImageSharp/Formats/Tiff/Constants/TiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Constants/TiffCompression.cs @@ -35,9 +35,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, diff --git a/src/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs b/src/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs index b545451412..b5548c707b 100644 --- a/src/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs +++ b/src/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs @@ -35,6 +35,16 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Constants /// public const ushort HeaderMagicNumber = 42; + /// + /// The big tiff header magic number + /// + public const ushort BigTiffHeaderMagicNumber = 43; + + /// + /// The big tiff bytesize of offsets value. + /// + public const ushort BigTiffBytesize = 8; + /// /// RowsPerStrip default value, which is effectively infinity. /// diff --git a/src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs b/src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs index 26cb12d425..cce1d278cc 100644 --- a/src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs +++ b/src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.IO; using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata.Profiles.Exif; namespace SixLabors.ImageSharp.Formats.Tiff @@ -14,23 +15,27 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// internal class DirectoryReader { - private readonly Stream stream; + private const int DirectoryMax = 65534; - private uint nextIfdOffset; + private readonly Stream stream; - private const int DirectoryMax = 65534; + private readonly MemoryAllocator allocator; - // used for sequential read big values (actual for multiframe big files) - // todo: different tags can link to the same data (stream offset) - investigate - private readonly SortedList lazyLoaders = new(new DuplicateKeyComparer()); + private ulong nextIfdOffset; - public DirectoryReader(Stream stream) => this.stream = stream; + public DirectoryReader(Stream stream, MemoryAllocator allocator) + { + this.stream = stream; + this.allocator = allocator; + } /// /// Gets the byte order. /// public ByteOrder ByteOrder { get; private set; } + public bool IsBigTiff { get; private set; } + /// /// Reads image file directories. /// @@ -38,14 +43,19 @@ namespace SixLabors.ImageSharp.Formats.Tiff public IEnumerable Read() { this.ByteOrder = ReadByteOrder(this.stream); - this.nextIfdOffset = new HeaderReader(this.stream, this.ByteOrder).ReadFileHeader(); - return this.ReadIfds(); + var headerReader = new HeaderReader(this.stream, this.ByteOrder); + headerReader.ReadFileHeader(); + + this.nextIfdOffset = headerReader.FirstIfdOffset; + this.IsBigTiff = headerReader.IsBigTiff; + + return this.ReadIfds(headerReader.IsBigTiff); } private static ByteOrder ReadByteOrder(Stream stream) { - var headerBytes = new byte[2]; - stream.Read(headerBytes, 0, 2); + Span headerBytes = stackalloc byte[2]; + stream.Read(headerBytes); if (headerBytes[0] == TiffConstants.ByteOrderLittleEndian && headerBytes[1] == TiffConstants.ByteOrderLittleEndian) { return ByteOrder.LittleEndian; @@ -59,16 +69,26 @@ namespace SixLabors.ImageSharp.Formats.Tiff throw TiffThrowHelper.ThrowInvalidHeader(); } - private IEnumerable ReadIfds() + private IEnumerable ReadIfds(bool isBigTiff) { var readers = new List(); - while (this.nextIfdOffset != 0 && this.nextIfdOffset < this.stream.Length) + while (this.nextIfdOffset != 0 && this.nextIfdOffset < (ulong)this.stream.Length) { - var reader = new EntryReader(this.stream, this.ByteOrder, this.nextIfdOffset, this.lazyLoaders); - reader.ReadTags(); + var reader = new EntryReader(this.stream, this.ByteOrder, this.allocator); + reader.ReadTags(isBigTiff, this.nextIfdOffset); - this.nextIfdOffset = reader.NextIfdOffset; + if (reader.BigValues.Count > 0) + { + reader.BigValues.Sort((t1, t2) => t1.Offset.CompareTo(t2.Offset)); + + // this means that most likely all elements are placed before next IFD + if (reader.BigValues[0].Offset < reader.NextIfdOffset) + { + reader.ReadBigValues(); + } + } + this.nextIfdOffset = reader.NextIfdOffset; readers.Add(reader); if (readers.Count >= DirectoryMax) @@ -77,36 +97,15 @@ namespace SixLabors.ImageSharp.Formats.Tiff } } - // Sequential reading big values. - foreach (Action loader in this.lazyLoaders.Values) - { - loader(); - } - - var list = new List(); + var list = new List(readers.Count); foreach (EntryReader reader in readers) { + reader.ReadBigValues(); var profile = new ExifProfile(reader.Values, reader.InvalidTags); list.Add(profile); } return list; } - - /// - /// used for possibility add a duplicate offsets (but tags don't duplicate). - /// - /// The type of the key. - private class DuplicateKeyComparer : IComparer - where TKey : IComparable - { - public int Compare(TKey x, TKey y) - { - int result = x.CompareTo(y); - - // Handle equality as being greater. - return (result == 0) ? 1 : result; - } - } } } diff --git a/src/ImageSharp/Formats/Tiff/Ifd/EntryReader.cs b/src/ImageSharp/Formats/Tiff/Ifd/EntryReader.cs index be4f59bffc..922b7bf078 100644 --- a/src/ImageSharp/Formats/Tiff/Ifd/EntryReader.cs +++ b/src/ImageSharp/Formats/Tiff/Ifd/EntryReader.cs @@ -1,64 +1,78 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. -using System; using System.Collections.Generic; using System.IO; using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata.Profiles.Exif; namespace SixLabors.ImageSharp.Formats.Tiff { internal class EntryReader : BaseExifReader { - private readonly uint startOffset; - - private readonly SortedList lazyLoaders; - - public EntryReader(Stream stream, ByteOrder byteOrder, uint ifdOffset, SortedList lazyLoaders) - : base(stream) - { + public EntryReader(Stream stream, ByteOrder byteOrder, MemoryAllocator allocator) + : base(stream, allocator) => this.IsBigEndian = byteOrder == ByteOrder.BigEndian; - this.startOffset = ifdOffset; - this.lazyLoaders = lazyLoaders; - } public List Values { get; } = new(); - public uint NextIfdOffset { get; private set; } + public ulong NextIfdOffset { get; private set; } - public void ReadTags() + public void ReadTags(bool isBigTiff, ulong ifdOffset) { - this.ReadValues(this.Values, this.startOffset); - this.NextIfdOffset = this.ReadUInt32(); + if (!isBigTiff) + { + this.ReadValues(this.Values, (uint)ifdOffset); + this.NextIfdOffset = this.ReadUInt32(); + + this.ReadSubIfd(this.Values); + } + else + { + this.ReadValues64(this.Values, ifdOffset); + this.NextIfdOffset = this.ReadUInt64(); - this.ReadSubIfd(this.Values); + //// this.ReadSubIfd64(this.Values); + } } - protected override void RegisterExtLoader(uint offset, Action reader) => - this.lazyLoaders.Add(offset, reader); + public void ReadBigValues() => this.ReadBigValues(this.Values); } internal class HeaderReader : BaseExifReader { public HeaderReader(Stream stream, ByteOrder byteOrder) - : base(stream) => + : base(stream, null) => this.IsBigEndian = byteOrder == ByteOrder.BigEndian; - public uint FirstIfdOffset { get; private set; } + public bool IsBigTiff { get; private set; } - public uint ReadFileHeader() + public ulong FirstIfdOffset { get; private set; } + + public void ReadFileHeader() { ushort magic = this.ReadUInt16(); - if (magic != TiffConstants.HeaderMagicNumber) + if (magic == TiffConstants.HeaderMagicNumber) { - TiffThrowHelper.ThrowInvalidHeader(); + this.IsBigTiff = false; + this.FirstIfdOffset = this.ReadUInt32(); + return; } + else if (magic == TiffConstants.BigTiffHeaderMagicNumber) + { + this.IsBigTiff = true; - this.FirstIfdOffset = this.ReadUInt32(); - return this.FirstIfdOffset; - } + ushort bytesize = this.ReadUInt16(); + ushort reserve = this.ReadUInt16(); + if (bytesize == TiffConstants.BigTiffBytesize && reserve == 0) + { + this.FirstIfdOffset = this.ReadUInt64(); + return; + } + } - protected override void RegisterExtLoader(uint offset, Action reader) => throw new NotSupportedException(); + TiffThrowHelper.ThrowInvalidHeader(); + } } } diff --git a/src/ImageSharp/Formats/Tiff/README.md b/src/ImageSharp/Formats/Tiff/README.md index ff4aa52b0c..488701e318 100644 --- a/src/ImageSharp/Formats/Tiff/README.md +++ b/src/ImageSharp/Formats/Tiff/README.md @@ -45,7 +45,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 | | diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 55af87005a..05c5358f59 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using System; using System.Buffers; using System.Collections.Generic; using System.Linq; @@ -12,6 +13,7 @@ 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 @@ -41,6 +43,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// private ByteOrder byteOrder; + /// + /// Indicating whether is BigTiff format. + /// + private bool isBigTiff; + /// /// Initializes a new instance of the class. /// @@ -141,10 +148,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff where TPixel : unmanaged, IPixel { this.inputStream = stream; - var reader = new DirectoryReader(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) @@ -154,7 +162,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff frames.Add(frame); } - ImageMetadata metadata = TiffDecoderMetadataCreator.Create(frames, this.ignoreMetadata, reader.ByteOrder); + ImageMetadata metadata = TiffDecoderMetadataCreator.Create(frames, this.ignoreMetadata, reader.ByteOrder, reader.IsBigTiff); // TODO: Tiff frames can have different sizes ImageFrame root = frames[0]; @@ -174,13 +182,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { this.inputStream = stream; - var reader = new DirectoryReader(stream); + var reader = new DirectoryReader(stream, this.Configuration.MemoryAllocator); IEnumerable directories = reader.Read(); ExifProfile rootFrameExifProfile = directories.First(); var rootMetadata = TiffFrameMetadata.Parse(rootFrameExifProfile); - ImageMetadata metadata = TiffDecoderMetadataCreator.Create(reader.ByteOrder, rootFrameExifProfile); + ImageMetadata metadata = TiffDecoderMetadataCreator.Create(reader.ByteOrder, reader.IsBigTiff, rootFrameExifProfile); int width = GetImageWidth(rootFrameExifProfile); int height = GetImageHeight(rootFrameExifProfile); @@ -197,9 +205,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff private ImageFrame DecodeFrame(ExifProfile tags, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - ImageFrameMetadata imageFrameMetaData = this.ignoreMetadata ? - new ImageFrameMetadata() : - new ImageFrameMetadata { ExifProfile = tags, XmpProfile = tags.GetValue(ExifTag.XMP)?.Value }; + var imageFrameMetaData = new ImageFrameMetadata(); + if (!this.ignoreMetadata) + { + imageFrameMetaData.ExifProfile = tags; + } TiffFrameMetadata tiffFrameMetaData = imageFrameMetaData.GetTiffMetadata(); TiffFrameMetadata.Parse(tiffFrameMetaData, tags); @@ -211,21 +221,58 @@ namespace SixLabors.ImageSharp.Formats.Tiff var frame = new ImageFrame(this.Configuration, width, height, imageFrameMetaData); int rowsPerStrip = tags.GetValue(ExifTag.RowsPerStrip) != null ? (int)tags.GetValue(ExifTag.RowsPerStrip).Value : TiffConstants.RowsPerStripInfinity; - Number[] stripOffsets = tags.GetValue(ExifTag.StripOffsets)?.Value; - Number[] stripByteCounts = tags.GetValue(ExifTag.StripByteCounts)?.Value; + + 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); if (this.PlanarConfiguration == TiffPlanarConfiguration.Planar) { - this.DecodeStripsPlanar(frame, rowsPerStrip, stripOffsets, stripByteCounts, cancellationToken); + this.DecodeStripsPlanar( + frame, + rowsPerStrip, + stripOffsets, + stripByteCounts, + cancellationToken); } else { - this.DecodeStripsChunky(frame, rowsPerStrip, stripOffsets, stripByteCounts, cancellationToken); + this.DecodeStripsChunky( + frame, + rowsPerStrip, + stripOffsets, + stripByteCounts, + cancellationToken); } + stripOffsetsMemory?.Dispose(); + stripByteCountsMemory?.Dispose(); return frame; } + private IMemoryOwner ConvertNumbers(Array array, out Span span) + { + if (array is Number[] numbers) + { + IMemoryOwner memory = this.memoryAllocator.Allocate(numbers.Length); + span = memory.GetSpan(); + for (int i = 0; i < numbers.Length; i++) + { + span[i] = (uint)numbers[i]; + } + + return memory; + } + else + { + DebugGuard.IsTrue(array is ulong[], $"Expected {nameof(UInt64)} array."); + span = (ulong[])array; + return null; + } + } + /// /// Calculates the size (in bytes) for a pixel buffer using the determined color format. /// @@ -276,7 +323,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// An array of byte offsets to each strip in the image. /// An array of the size of each strip (in bytes). /// The token to monitor cancellation. - private void DecodeStripsPlanar(ImageFrame frame, int rowsPerStrip, Number[] stripOffsets, Number[] stripByteCounts, CancellationToken cancellationToken) + private void DecodeStripsPlanar(ImageFrame frame, int rowsPerStrip, Span stripOffsets, Span stripByteCounts, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { int stripsPerPixel = this.BitsPerSample.Channels; @@ -329,10 +376,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff { decompressor.Decompress( this.inputStream, - (uint)stripOffsets[stripIndex], - (uint)stripByteCounts[stripIndex], + stripOffsets[stripIndex], + stripByteCounts[stripIndex], stripHeight, stripBuffers[planeIndex].GetSpan()); + stripIndex += stripsPerPlane; } @@ -357,7 +405,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// The strip offsets. /// The strip byte counts. /// The token to monitor cancellation. - private void DecodeStripsChunky(ImageFrame frame, int rowsPerStrip, Number[] stripOffsets, Number[] stripByteCounts, CancellationToken cancellationToken) + private void DecodeStripsChunky(ImageFrame frame, int rowsPerStrip, Span stripOffsets, Span stripByteCounts, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { // If the rowsPerStrip has the default value, which is effectively infinity. That is, the entire image is one strip. @@ -370,7 +418,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff int bitsPerPixel = this.BitsPerPixel; using IMemoryOwner stripBuffer = this.memoryAllocator.Allocate(uncompressedStripSize, AllocationOptions.Clean); - System.Span stripBufferSpan = stripBuffer.GetSpan(); + Span stripBufferSpan = stripBuffer.GetSpan(); Buffer2D pixels = frame.PixelBuffer; using TiffBaseDecompressor decompressor = TiffDecompressorsFactory.Create( @@ -413,7 +461,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff break; } - decompressor.Decompress(this.inputStream, (uint)stripOffsets[stripIndex], (uint)stripByteCounts[stripIndex], stripHeight, stripBufferSpan); + decompressor.Decompress( + this.inputStream, + stripOffsets[stripIndex], + stripByteCounts[stripIndex], + stripHeight, + stripBufferSpan); colorDecoder.Decode(stripBufferSpan, pixels, 0, top, frame.Width, stripHeight); } @@ -432,6 +485,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff TiffThrowHelper.ThrowImageFormatException("The TIFF image frame is missing the ImageWidth"); } + DebugGuard.MustBeLessThanOrEqualTo((ulong)width.Value, (ulong)int.MaxValue, nameof(ExifTag.ImageWidth)); + return (int)width.Value; } diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs index 6f8a81a827..ddbfbcb48a 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs @@ -9,6 +9,7 @@ using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Iptc; +using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Tiff @@ -18,7 +19,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// internal static class TiffDecoderMetadataCreator { - public static ImageMetadata Create(List> frames, bool ignoreMetadata, ByteOrder byteOrder) + public static ImageMetadata Create(List> frames, bool ignoreMetadata, ByteOrder byteOrder, bool isBigTiff) where TPixel : unmanaged, IPixel { if (frames.Count < 1) @@ -26,13 +27,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff TiffThrowHelper.ThrowImageFormatException("Expected at least one frame."); } - var imageMetaData = new ImageMetadata(); - ExifProfile exifProfileRootFrame = frames[0].Metadata.ExifProfile; - - SetResolution(imageMetaData, exifProfileRootFrame); - - TiffMetadata tiffMetadata = imageMetaData.GetTiffMetadata(); - tiffMetadata.ByteOrder = byteOrder; + ImageMetadata imageMetaData = Create(byteOrder, isBigTiff, frames[0].Metadata.ExifProfile); if (!ignoreMetadata) { @@ -45,6 +40,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff frameMetaData.IptcProfile = new IptcProfile(iptcBytes); } + IExifValue xmpProfileBytes = frameMetaData.ExifProfile.GetValue(ExifTag.XMP); + if (xmpProfileBytes != null) + { + frameMetaData.XmpProfile = new XmpProfile(xmpProfileBytes.Value); + } + IExifValue iccProfileBytes = frameMetaData.ExifProfile.GetValue(ExifTag.IccProfile); if (iccProfileBytes != null) { @@ -56,13 +57,14 @@ namespace SixLabors.ImageSharp.Formats.Tiff return imageMetaData; } - public static ImageMetadata Create(ByteOrder byteOrder, ExifProfile exifProfile) + public static ImageMetadata Create(ByteOrder byteOrder, bool isBigTiff, ExifProfile exifProfile) { var imageMetaData = new ImageMetadata(); SetResolution(imageMetaData, exifProfile); TiffMetadata tiffMetadata = imageMetaData.GetTiffMetadata(); tiffMetadata.ByteOrder = byteOrder; + tiffMetadata.FormatType = isBigTiff ? TiffFormatType.BigTIFF : TiffFormatType.Default; return imageMetaData; } diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs index a8420fabba..a187d444ac 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs @@ -24,12 +24,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// The IFD entries container to read the image format information for current frame. public static void VerifyAndParse(this TiffDecoderCore options, ExifProfile exifProfile, TiffFrameMetadata frameMetadata) { - if (exifProfile.GetValue(ExifTag.TileOffsets)?.Value != null) + if (exifProfile.GetValueInternal(ExifTag.TileOffsets) is not null || exifProfile.GetValueInternal(ExifTag.TileByteCounts) is not null) { TiffThrowHelper.ThrowNotSupported("Tiled images are not supported."); } - if (exifProfile.GetValue(ExifTag.ExtraSamples)?.Value != null) + if (exifProfile.GetValueInternal(ExifTag.ExtraSamples) is not null) { TiffThrowHelper.ThrowNotSupported("ExtraSamples is not supported."); } @@ -95,12 +95,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff private static void VerifyRequiredFieldsArePresent(ExifProfile exifProfile, TiffFrameMetadata frameMetadata) { - if (exifProfile.GetValue(ExifTag.StripOffsets) == null) + if (exifProfile.GetValueInternal(ExifTag.StripOffsets) is null) { TiffThrowHelper.ThrowImageFormatException("StripOffsets are missing and are required for decoding the TIFF image!"); } - if (exifProfile.GetValue(ExifTag.StripByteCounts) == null) + if (exifProfile.GetValueInternal(ExifTag.StripByteCounts) is null) { TiffThrowHelper.ThrowImageFormatException("StripByteCounts are missing and are required for decoding the TIFF image!"); } @@ -384,7 +384,8 @@ namespace SixLabors.ImageSharp.Formats.Tiff private static void ParseCompression(this TiffDecoderCore options, TiffCompression? compression, ExifProfile exifProfile) { - switch (compression) + // Default 1 (No compression) https://www.awaresystems.be/imaging/tiff/tifftags/compression.html + switch (compression ?? TiffCompression.None) { case TiffCompression.None: { @@ -441,7 +442,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff default: { - TiffThrowHelper.ThrowNotSupported($"The specified TIFF compression format {compression} is not supported"); + TiffThrowHelper.ThrowNotSupported($"The specified TIFF compression format '{compression}' is not supported"); break; } } diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs index 55dd7d3973..04f3682b02 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs @@ -6,6 +6,7 @@ using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Metadata.Profiles.Xmp; namespace SixLabors.ImageSharp.Formats.Tiff { @@ -57,9 +58,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff { ImageFrame rootFrame = image.Frames.RootFrame; ExifProfile rootFrameExifProfile = rootFrame.Metadata.ExifProfile ?? new ExifProfile(); - byte[] foorFrameXmpBytes = rootFrame.Metadata.XmpProfile; + XmpProfile rootFrameXmpProfile = rootFrame.Metadata.XmpProfile; - this.ProcessProfiles(image.Metadata, rootFrameExifProfile, foorFrameXmpBytes); + this.ProcessProfiles(image.Metadata, rootFrameExifProfile, rootFrameXmpProfile); this.ProcessMetadata(rootFrameExifProfile); if (!this.Collector.Entries.Exists(t => t.Tag == ExifTag.Software)) @@ -149,7 +150,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff } } - private void ProcessProfiles(ImageMetadata imageMetadata, ExifProfile exifProfile, byte[] xmpProfile) + private void ProcessProfiles(ImageMetadata imageMetadata, ExifProfile exifProfile, XmpProfile xmpProfile) { if (exifProfile != null && exifProfile.Parts != ExifParts.None) { @@ -203,7 +204,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff { var xmp = new ExifByteArray(ExifTagValue.XMP, ExifDataType.Byte) { - Value = xmpProfile + Value = xmpProfile.Data }; this.Collector.Add(xmp); @@ -394,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/TiffFormatType.cs b/src/ImageSharp/Formats/Tiff/TiffFormatType.cs new file mode 100644 index 0000000000..320386e4c8 --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/TiffFormatType.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.Tiff +{ + /// + /// The TIFF format type enum. + /// + public enum TiffFormatType + { + /// + /// The TIFF file format type. + /// + Default, + + /// + /// The BigTIFF format type. + /// + BigTIFF + } +} diff --git a/src/ImageSharp/Formats/Tiff/TiffImageFormatDetector.cs b/src/ImageSharp/Formats/Tiff/TiffImageFormatDetector.cs index fc2ad3e138..51280b4bf3 100644 --- a/src/ImageSharp/Formats/Tiff/TiffImageFormatDetector.cs +++ b/src/ImageSharp/Formats/Tiff/TiffImageFormatDetector.cs @@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff public sealed class TiffImageFormatDetector : IImageFormatDetector { /// - public int HeaderSize => 4; + public int HeaderSize => 8; /// public IImageFormat DetectFormat(ReadOnlySpan header) @@ -26,9 +26,42 @@ namespace SixLabors.ImageSharp.Formats.Tiff private bool IsSupportedFileFormat(ReadOnlySpan header) { - return header.Length >= this.HeaderSize && - ((header[0] == 0x49 && header[1] == 0x49 && header[2] == 0x2A && header[3] == 0x00) || // Little-endian - (header[0] == 0x4D && header[1] == 0x4D && header[2] == 0x00 && header[3] == 0x2A)); // Big-endian + if (header.Length >= this.HeaderSize) + { + if (header[0] == 0x49 && header[1] == 0x49) + { + // Little-endian + if (header[2] == 0x2A && header[3] == 0x00) + { + // tiff + return true; + } + else if (header[2] == 0x2B && header[3] == 0x00 + && header[4] == 8 && header[5] == 0 && header[6] == 0 && header[7] == 0) + { + // big tiff + return true; + } + } + else if (header[0] == 0x4D && header[1] == 0x4D) + { + // Big-endian + if (header[2] == 0 && header[3] == 0x2A) + { + // tiff + return true; + } + else + if (header[2] == 0 && header[3] == 0x2B + && header[4] == 0 && header[5] == 8 && header[6] == 0 && header[7] == 0) + { + // big tiff + return true; + } + } + } + + return false; } } } diff --git a/src/ImageSharp/Formats/Tiff/TiffMetadata.cs b/src/ImageSharp/Formats/Tiff/TiffMetadata.cs index cf1ab37545..f5124a78ae 100644 --- a/src/ImageSharp/Formats/Tiff/TiffMetadata.cs +++ b/src/ImageSharp/Formats/Tiff/TiffMetadata.cs @@ -26,6 +26,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// public ByteOrder ByteOrder { get; set; } + /// + /// Gets or sets the format type. + /// + public TiffFormatType FormatType { get; set; } + /// public IDeepCloneable DeepClone() => new TiffMetadata(this); } diff --git a/src/ImageSharp/Formats/Tiff/TiffThrowHelper.cs b/src/ImageSharp/Formats/Tiff/TiffThrowHelper.cs index 3c541a786d..8223c445ae 100644 --- a/src/ImageSharp/Formats/Tiff/TiffThrowHelper.cs +++ b/src/ImageSharp/Formats/Tiff/TiffThrowHelper.cs @@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// /// The error message for the exception. [MethodImpl(MethodImplOptions.NoInlining)] - public static void ThrowImageFormatException(string errorMessage) => throw new ImageFormatException(errorMessage); + public static Exception ThrowImageFormatException(string errorMessage) => throw new ImageFormatException(errorMessage); [MethodImpl(InliningOptions.ColdPath)] public static Exception NotSupportedDecompressor(string compressionType) => throw new NotSupportedException($"Not supported decoder compression method: {compressionType}"); diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs index 5fec09ef14..f21a465cd2 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/AlphaDecoder.cs b/src/ImageSharp/Formats/Webp/AlphaDecoder.cs index e63cd27b51..b1e155c1d6 100644 --- a/src/ImageSharp/Formats/Webp/AlphaDecoder.cs +++ b/src/ImageSharp/Formats/Webp/AlphaDecoder.cs @@ -9,6 +9,10 @@ using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Webp.BitReader; using SixLabors.ImageSharp.Formats.Webp.Lossless; using SixLabors.ImageSharp.Memory; +#if SUPPORTS_RUNTIME_INTRINSICS +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +#endif namespace SixLabors.ImageSharp.Formats.Webp { @@ -307,34 +311,94 @@ namespace SixLabors.ImageSharp.Formats.Webp private static void HorizontalUnfilter(Span prev, Span input, Span dst, int width) { - byte pred = (byte)(prev == null ? 0 : prev[0]); +#if SUPPORTS_RUNTIME_INTRINSICS + if (Sse2.IsSupported) + { + dst[0] = (byte)(input[0] + (prev.IsEmpty ? 0 : prev[0])); + if (width <= 1) + { + return; + } + + nint i; + Vector128 last = Vector128.Zero.WithElement(0, dst[0]); + ref byte srcRef = ref MemoryMarshal.GetReference(input); + for (i = 1; i + 8 <= width; i += 8) + { + var a0 = Vector128.Create(Unsafe.As(ref Unsafe.Add(ref srcRef, i)), 0); + Vector128 a1 = Sse2.Add(a0.AsByte(), last.AsByte()); + Vector128 a2 = Sse2.ShiftLeftLogical128BitLane(a1, 1); + Vector128 a3 = Sse2.Add(a1, a2); + Vector128 a4 = Sse2.ShiftLeftLogical128BitLane(a3, 2); + Vector128 a5 = Sse2.Add(a3, a4); + Vector128 a6 = Sse2.ShiftLeftLogical128BitLane(a5, 4); + Vector128 a7 = Sse2.Add(a5, a6); + + ref byte outputRef = ref Unsafe.Add(ref MemoryMarshal.GetReference(dst), i); + Unsafe.As>(ref outputRef) = a7.GetLower(); + last = Sse2.ShiftRightLogical(a7.AsInt64(), 56).AsInt32(); + } - for (int i = 0; i < width; i++) + for (; i < width; ++i) + { + dst[(int)i] = (byte)(input[(int)i] + dst[(int)i - 1]); + } + } + else +#endif { - byte val = (byte)(pred + input[i]); - pred = val; - dst[i] = val; + byte pred = (byte)(prev.IsEmpty ? 0 : prev[0]); + + for (int i = 0; i < width; i++) + { + byte val = (byte)(pred + input[i]); + pred = val; + dst[i] = val; + } } } private static void VerticalUnfilter(Span prev, Span input, Span dst, int width) { - if (prev == null) + if (prev.IsEmpty) { HorizontalUnfilter(null, input, dst, width); } else { - for (int i = 0; i < width; i++) +#if SUPPORTS_RUNTIME_INTRINSICS + if (Avx2.IsSupported) { - dst[i] = (byte)(prev[i] + input[i]); + nint i; + int maxPos = width & ~31; + for (i = 0; i < maxPos; i += 32) + { + Vector256 a0 = Unsafe.As>(ref Unsafe.Add(ref MemoryMarshal.GetReference(input), i)); + Vector256 b0 = Unsafe.As>(ref Unsafe.Add(ref MemoryMarshal.GetReference(prev), i)); + Vector256 c0 = Avx2.Add(a0.AsByte(), b0.AsByte()); + ref byte outputRef = ref Unsafe.Add(ref MemoryMarshal.GetReference(dst), i); + Unsafe.As>(ref outputRef) = c0; + } + + for (; i < width; i++) + { + dst[(int)i] = (byte)(prev[(int)i] + input[(int)i]); + } + } + else +#endif + { + for (int i = 0; i < width; i++) + { + dst[i] = (byte)(prev[i] + input[i]); + } } } } private static void GradientUnfilter(Span prev, Span input, Span dst, int width) { - if (prev == null) + if (prev.IsEmpty) { HorizontalUnfilter(null, input, dst, width); } diff --git a/src/ImageSharp/Formats/Webp/AlphaEncoder.cs b/src/ImageSharp/Formats/Webp/AlphaEncoder.cs new file mode 100644 index 0000000000..1019073d87 --- /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 9208881360..fc1accfdee 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.Xmp; namespace SixLabors.ImageSharp.Formats.Webp.BitWriter { @@ -46,6 +47,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. /// @@ -90,34 +97,79 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter } /// - /// Calculates the exif chunk size. + /// Calculates the chunk size of EXIF or XMP metadata. + /// + /// The metadata profile bytes. + /// The metadata chunk size in bytes. + protected uint MetadataChunkSize(byte[] metadataBytes) + { + uint metaSize = (uint)metadataBytes.Length; + uint metaChunkSize = WebpConstants.ChunkHeaderSize + metaSize + (metaSize & 1); + + return metaChunkSize; + } + + /// + /// Calculates the chunk size of a alpha chunk. /// - /// The exif profile bytes. - /// The exif chunk size in bytes. - protected uint ExifChunkSize(byte[] exifBytes) + /// The alpha chunk bytes. + /// The alpha data chunk size in bytes. + protected uint AlphaChunkSize(Span alphaBytes) { - uint exifSize = (uint)exifBytes.Length; - uint exifChunkSize = WebpConstants.ChunkHeaderSize + exifSize + (exifSize & 1); + uint alphaSize = (uint)alphaBytes.Length + 1; + uint alphaChunkSize = WebpConstants.ChunkHeaderSize + alphaSize + (alphaSize & 1); - return exifChunkSize; + return alphaChunkSize; } /// - /// Writes the Exif profile to the stream. + /// Writes a metadata profile (EXIF or XMP) to the stream. /// /// The stream to write to. - /// The exif profile bytes. - protected void WriteExifProfile(Stream stream, byte[] exifBytes) + /// The metadata profile's bytes. + /// The chuck type to write. + protected void WriteMetadataProfile(Stream stream, byte[] metadataBytes, WebpChunkType chunkType) { - DebugGuard.NotNull(exifBytes, nameof(exifBytes)); + DebugGuard.NotNull(metadataBytes, nameof(metadataBytes)); + + uint size = (uint)metadataBytes.Length; + Span buf = this.scratchBuffer.AsSpan(0, 4); + BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)chunkType); + stream.Write(buf); + BinaryPrimitives.WriteUInt32LittleEndian(buf, size); + stream.Write(buf); + stream.Write(metadataBytes); + + // Add padding byte if needed. + if ((size & 1) == 1) + { + stream.WriteByte(0); + } + } - uint size = (uint)exifBytes.Length; + /// + /// 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.Exif); + BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Alpha); stream.Write(buf); BinaryPrimitives.WriteUInt32LittleEndian(buf, size); stream.Write(buf); - stream.Write(exifBytes); + + byte flags = 0; + if (alphaDataIsCompressed) + { + flags |= 1; + } + + stream.WriteByte(flags); + stream.Write(dataBytes); // Add padding byte if needed. if ((size & 1) == 1) @@ -131,10 +183,11 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter /// /// 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 width of the image. /// The height of the image. /// Flag indicating, if a alpha channel is present. - protected void WriteVp8XHeader(Stream stream, ExifProfile exifProfile, uint width, uint height, bool hasAlpha) + protected void WriteVp8XHeader(Stream stream, ExifProfile exifProfile, XmpProfile xmpProfile, uint width, uint height, bool hasAlpha) { if (width > MaxDimension || height > MaxDimension) { @@ -154,6 +207,12 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter flags |= 8; } + if (xmpProfile != null) + { + // Set xmp bit. + flags |= 4; + } + if (hasAlpha) { // Set alpha bit. diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs index 3b2f943db5..fa6e09d875 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.Xmp; namespace SixLabors.ImageSharp.Formats.Webp.BitWriter { @@ -404,20 +405,49 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter /// /// The stream to write to. /// The exif profile. + /// The XMP 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, 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, + uint width, + uint height, + bool hasAlpha, + Span alphaData, + bool alphaDataIsCompressed) { bool isVp8X = false; byte[] exifBytes = null; + byte[] xmpBytes = null; uint riffSize = 0; if (exifProfile != null) { isVp8X = true; - riffSize += ExtendedFileChunkSize; exifBytes = exifProfile.ToByteArray(); - riffSize += this.ExifChunkSize(exifBytes); + riffSize += this.MetadataChunkSize(exifBytes); + } + + if (xmpProfile != null) + { + isVp8X = true; + xmpBytes = xmpProfile.Data; + riffSize += this.MetadataChunkSize(xmpBytes); + } + + if (hasAlpha) + { + isVp8X = true; + riffSize += this.AlphaChunkSize(alphaData); + } + + if (isVp8X) + { + riffSize += ExtendedFileChunkSize; } this.Finish(); @@ -440,7 +470,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter riffSize += WebpConstants.TagSize + WebpConstants.ChunkHeaderSize + vp8Size; // Emit headers and partition #0 - this.WriteWebpHeaders(stream, size0, vp8Size, riffSize, isVp8X, width, height, exifProfile, hasAlpha); + this.WriteWebpHeaders(stream, size0, vp8Size, riffSize, isVp8X, width, height, exifProfile, xmpProfile, hasAlpha, alphaData, alphaDataIsCompressed); bitWriterPartZero.WriteToStream(stream); // Write the encoded image to the stream. @@ -452,7 +482,12 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter if (exifProfile != null) { - this.WriteExifProfile(stream, exifBytes); + this.WriteMetadataProfile(stream, exifBytes, WebpChunkType.Exif); + } + + if (xmpProfile != null) + { + this.WriteMetadataProfile(stream, xmpBytes, WebpChunkType.Xmp); } } @@ -623,14 +658,30 @@ 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, bool hasAlpha) + private void WriteWebpHeaders( + Stream stream, + uint size0, + uint vp8Size, + uint riffSize, + bool isVp8X, + uint width, + uint height, + ExifProfile exifProfile, + XmpProfile xmpProfile, + bool hasAlpha, + Span alphaData, + bool alphaDataIsCompressed) { this.WriteRiffHeader(stream, riffSize); // Write VP8X, header if necessary. if (isVp8X) { - this.WriteVp8XHeader(stream, exifProfile, width, height, hasAlpha); + this.WriteVp8XHeader(stream, exifProfile, xmpProfile, width, height, hasAlpha); + 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 b83865aa36..d41224f908 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.Xmp; namespace SixLabors.ImageSharp.Formats.Webp.BitWriter { @@ -132,20 +133,30 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter /// /// The stream to write to. /// The exif profile. + /// The XMP 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, uint width, uint height, bool hasAlpha) + public void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, XmpProfile xmpProfile, uint width, uint height, bool hasAlpha) { bool isVp8X = false; byte[] exifBytes = null; + byte[] xmpBytes = null; uint riffSize = 0; if (exifProfile != null) { isVp8X = true; riffSize += ExtendedFileChunkSize; exifBytes = exifProfile.ToByteArray(); - riffSize += this.ExifChunkSize(exifBytes); + riffSize += this.MetadataChunkSize(exifBytes); + } + + if (xmpProfile != null) + { + isVp8X = true; + riffSize += ExtendedFileChunkSize; + xmpBytes = xmpProfile.Data; + riffSize += this.MetadataChunkSize(xmpBytes); } this.Finish(); @@ -160,7 +171,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter // Write VP8X, header if necessary. if (isVp8X) { - this.WriteVp8XHeader(stream, exifProfile, width, height, hasAlpha); + this.WriteVp8XHeader(stream, exifProfile, xmpProfile, width, height, hasAlpha); } // Write magic bytes indicating its a lossless webp. @@ -180,7 +191,12 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter if (exifProfile != null) { - this.WriteExifProfile(stream, exifBytes); + this.WriteMetadataProfile(stream, exifBytes, WebpChunkType.Exif); + } + + if (xmpProfile != null) + { + this.WriteMetadataProfile(stream, xmpBytes, WebpChunkType.Xmp); } } diff --git a/src/ImageSharp/Formats/Webp/IWebpEncoderOptions.cs b/src/ImageSharp/Formats/Webp/IWebpEncoderOptions.cs index 000de4f88c..57ec32753d 100644 --- a/src/ImageSharp/Formats/Webp/IWebpEncoderOptions.cs +++ b/src/ImageSharp/Formats/Webp/IWebpEncoderOptions.cs @@ -10,6 +10,7 @@ namespace SixLabors.ImageSharp.Formats.Webp { /// /// Gets the webp file format used. Either lossless or lossy. + /// Defaults to lossy. /// WebpFileFormatType? FileFormat { get; } @@ -30,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/LosslessUtils.cs b/src/ImageSharp/Formats/Webp/Lossless/LosslessUtils.cs index ebb198a2d8..e7782b0ef4 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/LosslessUtils.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/LosslessUtils.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Memory; diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index 8566566f60..30d65562ae 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -10,6 +10,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Webp.BitWriter; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Webp.Lossless @@ -227,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. @@ -235,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); @@ -252,7 +255,40 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless this.EncodeStream(image); // Write bytes from the bitwriter buffer to the stream. - this.bitWriter.WriteEncodedImageToStream(stream, image.Metadata.ExifProfile, (uint)width, (uint)height, hasAlpha); + this.bitWriter.WriteEncodedImageToStream(stream, metadata.ExifProfile, metadata.XmpProfile, (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/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index 37e09d0802..695359e5ea 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -7,6 +7,7 @@ using System.IO; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Webp.BitWriter; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Webp.Lossy @@ -70,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; @@ -104,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, @@ -113,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; @@ -124,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 @@ -173,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; } /// @@ -296,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; @@ -317,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); @@ -355,8 +372,17 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy this.AdjustFilterStrength(); // Write bytes from the bitwriter buffer to the stream. - image.Metadata.SyncProfiles(); - this.bitWriter.WriteEncodedImageToStream(stream, image.Metadata.ExifProfile, (uint)width, (uint)height, hasAlpha); + ImageMetadata metadata = image.Metadata; + metadata.SyncProfiles(); + this.bitWriter.WriteEncodedImageToStream( + stream, + metadata.ExifProfile, + metadata.XmpProfile, + (uint)width, + (uint)height, + hasAlpha, + alphaData, + this.alphaCompression && alphaCompressionSucceeded); } /// diff --git a/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs b/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs index 7a731f4284..878bebd105 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs @@ -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/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs index 09071406c5..9d18e5d821 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs @@ -13,6 +13,7 @@ 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; namespace SixLabors.ImageSharp.Formats.Webp @@ -177,6 +178,7 @@ namespace SixLabors.ImageSharp.Formats.Webp /// Reads an the extended webp file header. An extended file header consists of: /// - A 'VP8X' chunk with information about features used in the file. /// - An optional 'ICCP' chunk with color profile. + /// - An optional 'XMP' chunk with metadata. /// - An optional 'ANIM' chunk with animation control data. /// - An optional 'ALPH' chunk with alpha channel data. /// After the image header, image data will follow. After that optional image metadata chunks (EXIF and XMP) can follow. @@ -228,12 +230,27 @@ namespace SixLabors.ImageSharp.Formats.Webp this.buffer[3] = 0; uint height = (uint)BinaryPrimitives.ReadInt32LittleEndian(this.buffer) + 1; - // Optional chunks ICCP, ALPH and ANIM can follow here. - WebpChunkType chunkType = this.ReadChunkType(); - while (IsOptionalVp8XChunk(chunkType)) + // Read all the chunks in the order they occur. + var info = new WebpImageInfo(); + while (this.currentStream.Position < this.currentStream.Length) { - this.ParseOptionalExtendedChunks(chunkType, features); - chunkType = this.ReadChunkType(); + WebpChunkType chunkType = this.ReadChunkType(); + if (chunkType == WebpChunkType.Vp8) + { + info = this.ReadVp8Header(features); + } + else if (chunkType == WebpChunkType.Vp8L) + { + info = this.ReadVp8LHeader(features); + } + else if (IsOptionalVp8XChunk(chunkType)) + { + this.ParseOptionalExtendedChunks(chunkType, features); + } + else + { + WebpThrowHelper.ThrowImageFormatException("Unexpected chunk followed VP8X header"); + } } if (features.Animation) @@ -242,17 +259,7 @@ namespace SixLabors.ImageSharp.Formats.Webp return new WebpImageInfo() { Width = width, Height = height, Features = features }; } - switch (chunkType) - { - case WebpChunkType.Vp8: - return this.ReadVp8Header(features); - case WebpChunkType.Vp8L: - return this.ReadVp8LHeader(features); - } - - WebpThrowHelper.ThrowImageFormatException("Unexpected chunk followed VP8X header"); - - return new WebpImageInfo(); + return info; } /// @@ -413,7 +420,7 @@ namespace SixLabors.ImageSharp.Formats.Webp } /// - /// Parses optional VP8X chunks, which can be ICCP, ANIM or ALPH chunks. + /// Parses optional VP8X chunks, which can be ICCP, XMP, ANIM or ALPH chunks. /// /// The chunk type. /// The webp image features. @@ -440,6 +447,38 @@ namespace SixLabors.ImageSharp.Formats.Webp 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; + } + + 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; + } + + break; + case WebpChunkType.Animation: // TODO: Decoding animation is not implemented yet. break; @@ -451,6 +490,9 @@ namespace SixLabors.ImageSharp.Formats.Webp features.AlphaData = this.memoryAllocator.Allocate(alphaDataSize); this.currentStream.Read(features.AlphaData.Memory.Span, 0, alphaDataSize); break; + default: + WebpThrowHelper.ThrowImageFormatException("Unexpected chunk followed VP8X header"); + break; } } @@ -530,7 +572,9 @@ namespace SixLabors.ImageSharp.Formats.Webp { WebpChunkType.Alpha => true, WebpChunkType.Animation => true, + WebpChunkType.Exif => true, WebpChunkType.Iccp => true, + WebpChunkType.Xmp => true, _ => false }; } diff --git a/src/ImageSharp/Formats/Webp/WebpEncoder.cs b/src/ImageSharp/Formats/Webp/WebpEncoder.cs index bdcbb194b1..d0b60d18cd 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 8640261b17..0fbff81fe4 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; @@ -70,6 +70,7 @@ namespace SixLabors.ImageSharp.Formats.Webp /// /// Indicating what file format compression should be used. + /// Defaults to lossy. /// private readonly WebpFileFormatType? fileFormat; @@ -99,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. @@ -112,43 +113,44 @@ namespace SixLabors.ImageSharp.Formats.Webp Guard.NotNull(stream, nameof(stream)); this.configuration = image.GetConfiguration(); - bool lossy; + bool lossless; if (this.fileFormat is not null) { - lossy = this.fileFormat == WebpFileFormatType.Lossy; + lossless = this.fileFormat == WebpFileFormatType.Lossless; } else { WebpMetadata webpMetadata = image.Metadata.GetWebpMetadata(); - lossy = webpMetadata.FileFormat == WebpFileFormatType.Lossy; + lossless = webpMetadata.FileFormat == WebpFileFormatType.Lossless; } - if (lossy) + if (lossless) { - using var enc = new Vp8Encoder( + using var enc = new Vp8LEncoder( this.memoryAllocator, this.configuration, image.Width, image.Height, this.quality, this.method, - this.entropyPasses, - this.filterStrength, - this.spatialNoiseShaping); + this.transparentColorMode, + this.nearLossless, + this.nearLosslessQuality); enc.Encode(image, stream); } else { - using var enc = new Vp8LEncoder( + using var enc = new Vp8Encoder( this.memoryAllocator, this.configuration, image.Width, image.Height, this.quality, this.method, - this.transparentColorMode, - this.nearLossless, - this.nearLosslessQuality); + this.entropyPasses, + this.filterStrength, + this.spatialNoiseShaping, + this.alphaCompression); enc.Encode(image, stream); } } diff --git a/src/ImageSharp/IO/BufferedReadStream.cs b/src/ImageSharp/IO/BufferedReadStream.cs index acba3eff0a..4ab7f312b2 100644 --- a/src/ImageSharp/IO/BufferedReadStream.cs +++ b/src/ImageSharp/IO/BufferedReadStream.cs @@ -87,7 +87,6 @@ namespace SixLabors.ImageSharp.IO set { Guard.MustBeGreaterThanOrEqualTo(value, 0, nameof(this.Position)); - Guard.MustBeLessThanOrEqualTo(value, this.Length, nameof(this.Position)); // Only reset readBufferIndex if we are out of bounds of our working buffer // otherwise we should simply move the value by the diff. diff --git a/src/ImageSharp/IO/ChunkedMemoryStream.cs b/src/ImageSharp/IO/ChunkedMemoryStream.cs index e28baf879d..e39930defe 100644 --- a/src/ImageSharp/IO/ChunkedMemoryStream.cs +++ b/src/ImageSharp/IO/ChunkedMemoryStream.cs @@ -168,7 +168,6 @@ namespace SixLabors.ImageSharp.IO // Position is out of range this.readChunk = backupReadChunk; this.readOffset = backupReadOffset; - ThrowArgumentOutOfRange(nameof(value)); } } } diff --git a/src/ImageSharp/Image.Decode.cs b/src/ImageSharp/Image.Decode.cs index 3e1ffe9baf..f09ee74413 100644 --- a/src/ImageSharp/Image.Decode.cs +++ b/src/ImageSharp/Image.Decode.cs @@ -35,8 +35,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); } diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj index 1b3a8e27f6..39c85c4f22 100644 --- a/src/ImageSharp/ImageSharp.csproj +++ b/src/ImageSharp/ImageSharp.csproj @@ -10,7 +10,7 @@ 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 @@ -49,6 +49,7 @@ + @@ -57,13 +58,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 61682aa567..0000000000 --- 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 0000000000..1c7d6cdfa9 --- /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 9302c67e7d..2ea76da957 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 666b248552..151fef69c4 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 5f0759f203..8221120240 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/Buffer2DRegion{T}.cs b/src/ImageSharp/Memory/Buffer2DRegion{T}.cs index d1c39ccbf5..13b3395977 100644 --- a/src/ImageSharp/Memory/Buffer2DRegion{T}.cs +++ b/src/ImageSharp/Memory/Buffer2DRegion{T}.cs @@ -88,7 +88,7 @@ namespace SixLabors.ImageSharp.Memory /// The row index /// The span [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Span GetRowSpan(int y) + public Span DangerousGetRowSpan(int y) { int yy = this.Rectangle.Y + y; int xx = this.Rectangle.X; @@ -152,7 +152,7 @@ namespace SixLabors.ImageSharp.Memory for (int y = 0; y < this.Rectangle.Height; y++) { - Span row = this.GetRowSpan(y); + Span row = this.DangerousGetRowSpan(y); row.Clear(); } } diff --git a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs index 21faf8e562..01aac3148e 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/ImageFrameMetadata.cs b/src/ImageSharp/Metadata/ImageFrameMetadata.cs index 1819fd2bc5..1cad4ebe86 100644 --- a/src/ImageSharp/Metadata/ImageFrameMetadata.cs +++ b/src/ImageSharp/Metadata/ImageFrameMetadata.cs @@ -1,12 +1,12 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. -using System; using System.Collections.Generic; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Iptc; +using SixLabors.ImageSharp.Metadata.Profiles.Xmp; namespace SixLabors.ImageSharp.Metadata { @@ -43,8 +43,7 @@ namespace SixLabors.ImageSharp.Metadata this.ExifProfile = other.ExifProfile?.DeepClone(); this.IccProfile = other.IccProfile?.DeepClone(); this.IptcProfile = other.IptcProfile?.DeepClone(); - this.XmpProfile = other.XmpProfile != null ? new byte[other.XmpProfile.Length] : null; - other.XmpProfile?.AsSpan().CopyTo(this.XmpProfile.AsSpan()); + this.XmpProfile = other.XmpProfile?.DeepClone(); } /// @@ -55,7 +54,7 @@ namespace SixLabors.ImageSharp.Metadata /// /// Gets or sets the XMP profile. /// - internal byte[] XmpProfile { get; set; } + public XmpProfile XmpProfile { get; set; } /// /// Gets or sets the list of ICC profiles. diff --git a/src/ImageSharp/Metadata/ImageMetadata.cs b/src/ImageSharp/Metadata/ImageMetadata.cs index 425fd9b47f..89592f776c 100644 --- a/src/ImageSharp/Metadata/ImageMetadata.cs +++ b/src/ImageSharp/Metadata/ImageMetadata.cs @@ -6,6 +6,7 @@ using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Iptc; +using SixLabors.ImageSharp.Metadata.Profiles.Xmp; namespace SixLabors.ImageSharp.Metadata { @@ -39,7 +40,7 @@ namespace SixLabors.ImageSharp.Metadata /// /// Initializes a new instance of the class. /// - internal ImageMetadata() + public ImageMetadata() { this.horizontalResolution = DefaultHorizontalResolution; this.verticalResolution = DefaultVerticalResolution; @@ -71,7 +72,7 @@ namespace SixLabors.ImageSharp.Metadata /// /// 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 @@ -89,7 +90,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 @@ -107,10 +108,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; } @@ -119,13 +138,18 @@ namespace SixLabors.ImageSharp.Metadata /// public ExifProfile ExifProfile { get; set; } + /// + /// Gets or sets the XMP profile. + /// + public XmpProfile XmpProfile { get; set; } + /// /// Gets or sets the list of ICC profiles. /// public IccProfile IccProfile { get; set; } /// - /// Gets or sets the iptc profile. + /// Gets or sets the IPTC profile. /// public IptcProfile IptcProfile { get; set; } 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 cd7141fc8d..22a1058168 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 0c81f14dd4..543e3d5c4d 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/ExifDataType.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifDataType.cs index 733eb4a798..0185afb509 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifDataType.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifDataType.cs @@ -80,6 +80,21 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif /// /// Reference to an IFD (32-bit (4-byte) unsigned integer). /// - Ifd = 13 + Ifd = 13, + + /// + /// A 64-bit (8-byte) unsigned integer. + /// + Long8 = 16, + + /// + /// A 64-bit (8-byte) signed integer (2's complement notation). + /// + SignedLong8 = 17, + + /// + /// Reference to an IFD (64-bit (8-byte) unsigned integer). + /// + Ifd8 = 18, } } diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifDataTypes.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifDataTypes.cs index 4f75999bbf..729efb3909 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifDataTypes.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifDataTypes.cs @@ -32,10 +32,14 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif case ExifDataType.Long: case ExifDataType.SignedLong: case ExifDataType.SingleFloat: + case ExifDataType.Ifd: return 4; case ExifDataType.DoubleFloat: case ExifDataType.Rational: case ExifDataType.SignedRational: + case ExifDataType.Long8: + case ExifDataType.SignedLong8: + case ExifDataType.Ifd8: return 8; default: throw new NotSupportedException(dataType.ToString()); diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifEncodedStringHelpers.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifEncodedStringHelpers.cs new file mode 100644 index 0000000000..5fd613b1f0 --- /dev/null +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifEncodedStringHelpers.cs @@ -0,0 +1,121 @@ +// 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) + { + fixed (char* c = value) + { + fixed (byte* b = destination) + { + return encoding.GetBytes(c, value.Length, b, destination.Length); + } + } + } + + 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/ExifReader.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs index 6e671b3eca..9b5e098c83 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Buffers; using System.Buffers.Binary; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -9,15 +10,19 @@ using System.Diagnostics; using System.IO; using System.Runtime.CompilerServices; using System.Text; +using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { internal class ExifReader : BaseExifReader { - private readonly List loaders = new List(); - public ExifReader(byte[] exifData) - : base(new MemoryStream(exifData ?? throw new ArgumentNullException(nameof(exifData)))) + : this(exifData, null) + { + } + + public ExifReader(byte[] exifData, MemoryAllocator allocator) + : base(new MemoryStream(exifData ?? throw new ArgumentNullException(nameof(exifData))), allocator) { } @@ -47,16 +52,11 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif this.ReadSubIfd(values); - foreach (Action loader in this.loaders) - { - loader(); - } + this.ReadBigValues(values); return values; } - protected override void RegisterExtLoader(uint offset, Action loader) => this.loaders.Add(loader); - private void GetThumbnail(uint offset) { if (offset == 0) @@ -86,19 +86,21 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif /// internal abstract class BaseExifReader { - private readonly byte[] offsetBuffer = new byte[4]; + private readonly byte[] buf8 = new byte[8]; private readonly byte[] buf4 = new byte[4]; private readonly byte[] buf2 = new byte[2]; + private readonly MemoryAllocator allocator; private readonly Stream data; private List invalidTags; - private uint exifOffset; - - private uint gpsOffset; + private List subIfds; - protected BaseExifReader(Stream stream) => + protected BaseExifReader(Stream stream, MemoryAllocator allocator) + { this.data = stream ?? throw new ArgumentNullException(nameof(stream)); + this.allocator = allocator; + } private delegate TDataType ConverterMethod(ReadOnlySpan data); @@ -119,7 +121,51 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif public bool IsBigEndian { get; protected set; } - protected abstract void RegisterExtLoader(uint offset, Action loader); + public List<(ulong Offset, ExifDataType DataType, ulong NumberOfComponents, ExifValue Exif)> BigValues { get; } = new(); + + protected void ReadBigValues(List values) + { + if (this.BigValues.Count == 0) + { + return; + } + + int maxSize = 0; + foreach ((ulong offset, ExifDataType dataType, ulong numberOfComponents, ExifValue exif) in this.BigValues) + { + ulong size = numberOfComponents * ExifDataTypes.GetSize(dataType); + DebugGuard.MustBeLessThanOrEqualTo(size, int.MaxValue, nameof(size)); + + if ((int)size > maxSize) + { + maxSize = (int)size; + } + } + + if (this.allocator != null) + { + // tiff, bigTiff + using IMemoryOwner memory = this.allocator.Allocate(maxSize); + Span buf = memory.GetSpan(); + foreach ((ulong Offset, ExifDataType DataType, ulong NumberOfComponents, ExifValue Exif) tag in this.BigValues) + { + ulong size = tag.NumberOfComponents * ExifDataTypes.GetSize(tag.DataType); + this.ReadBigValue(values, tag, buf.Slice(0, (int)size)); + } + } + else + { + // embedded exif + Span buf = maxSize <= 256 ? stackalloc byte[256] : new byte[maxSize]; + foreach ((ulong Offset, ExifDataType DataType, ulong NumberOfComponents, ExifValue Exif) tag in this.BigValues) + { + ulong size = tag.NumberOfComponents * ExifDataTypes.GetSize(tag.DataType); + this.ReadBigValue(values, tag, buf.Slice(0, (int)size)); + } + } + + this.BigValues.Clear(); + } /// /// Reads the values to the values collection. @@ -136,22 +182,45 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif this.Seek(offset); int count = this.ReadUInt16(); + Span offsetBuffer = stackalloc byte[4]; for (int i = 0; i < count; i++) { - this.ReadValue(values); + this.ReadValue(values, offsetBuffer); } } protected void ReadSubIfd(List values) { - if (this.exifOffset != 0) + if (this.subIfds is not null) + { + foreach (ulong subIfdOffset in this.subIfds) + { + this.ReadValues(values, (uint)subIfdOffset); + } + } + } + + 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."); + + this.Seek(offset); + ulong count = this.ReadUInt64(); + + Span offsetBuffer = stackalloc byte[8]; + for (ulong i = 0; i < count; i++) { - this.ReadValues(values, this.exifOffset); + this.ReadValue64(values, offsetBuffer); } + } - if (this.gpsOffset != 0) + protected void ReadBigValue(IList values, (ulong Offset, ExifDataType DataType, ulong NumberOfComponents, ExifValue Exif) tag, Span buffer) + { + this.Seek(tag.Offset); + if (this.TryReadSpan(buffer)) { - this.ReadValues(values, this.gpsOffset); + object value = this.ConvertValue(tag.DataType, buffer, tag.NumberOfComponents > 1 || tag.Exif.IsArray); + this.Add(values, tag.Exif, value); } } @@ -172,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); @@ -183,10 +250,12 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif buffer = buffer.Slice(0, nullCharIndex); } - return Encoding.UTF8.GetString(buffer); + return encoding.GetString(buffer); } - private object ConvertValue(ExifDataType dataType, ReadOnlySpan buffer, uint numberOfComponents) + private byte ConvertToByte(ReadOnlySpan buffer) => buffer[0]; + + private object ConvertValue(ExifDataType dataType, ReadOnlySpan buffer, bool isArray) { if (buffer.Length == 0) { @@ -198,90 +267,101 @@ 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: - if (numberOfComponents == 1) + case ExifDataType.Undefined: + if (!isArray) { return this.ConvertToByte(buffer); } return buffer.ToArray(); case ExifDataType.DoubleFloat: - if (numberOfComponents == 1) + if (!isArray) { return this.ConvertToDouble(buffer); } return ToArray(dataType, buffer, this.ConvertToDouble); case ExifDataType.Long: - if (numberOfComponents == 1) + case ExifDataType.Ifd: + if (!isArray) { return this.ConvertToUInt32(buffer); } return ToArray(dataType, buffer, this.ConvertToUInt32); case ExifDataType.Rational: - if (numberOfComponents == 1) + if (!isArray) { return this.ToRational(buffer); } return ToArray(dataType, buffer, this.ToRational); case ExifDataType.Short: - if (numberOfComponents == 1) + if (!isArray) { return this.ConvertToShort(buffer); } return ToArray(dataType, buffer, this.ConvertToShort); case ExifDataType.SignedByte: - if (numberOfComponents == 1) + if (!isArray) { return this.ConvertToSignedByte(buffer); } return ToArray(dataType, buffer, this.ConvertToSignedByte); case ExifDataType.SignedLong: - if (numberOfComponents == 1) + if (!isArray) { return this.ConvertToInt32(buffer); } return ToArray(dataType, buffer, this.ConvertToInt32); case ExifDataType.SignedRational: - if (numberOfComponents == 1) + if (!isArray) { return this.ToSignedRational(buffer); } return ToArray(dataType, buffer, this.ToSignedRational); case ExifDataType.SignedShort: - if (numberOfComponents == 1) + if (!isArray) { return this.ConvertToSignedShort(buffer); } return ToArray(dataType, buffer, this.ConvertToSignedShort); case ExifDataType.SingleFloat: - if (numberOfComponents == 1) + if (!isArray) { return this.ConvertToSingle(buffer); } return ToArray(dataType, buffer, this.ConvertToSingle); - case ExifDataType.Undefined: - if (numberOfComponents == 1) + case ExifDataType.Long8: + case ExifDataType.Ifd8: + if (!isArray) { - return this.ConvertToByte(buffer); + return this.ConvertToUInt64(buffer); } - return buffer.ToArray(); + return ToArray(dataType, buffer, this.ConvertToUInt64); + case ExifDataType.SignedLong8: + if (!isArray) + { + return this.ConvertToInt64(buffer); + } + + return ToArray(dataType, buffer, this.ConvertToUInt64); + default: - throw new NotSupportedException(); + throw new NotSupportedException($"Data type {dataType} is not supported."); } } - private void ReadValue(List values) + private void ReadValue(List values, Span offsetBuffer) { // 2 | 2 | 4 | 4 // tag | type | count | value offset @@ -295,7 +375,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif uint numberOfComponents = this.ReadUInt32(); - this.TryReadSpan(this.offsetBuffer); + this.TryReadSpan(offsetBuffer); // Ensure that the data type is valid if (dataType == ExifDataType.Unknown) @@ -305,9 +385,9 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif // Issue #132: ExifDataType == Undefined is treated like a byte array. // If numberOfComponents == 0 this value can only be handled as an inline value and must fallback to 4 (bytes) - if (dataType == ExifDataType.Undefined && numberOfComponents == 0) + if (numberOfComponents == 0) { - numberOfComponents = 4; + numberOfComponents = 4 / ExifDataTypes.GetSize(dataType); } ExifValue exifValue = ExifValues.Create(tag) ?? ExifValues.Create(tag, dataType, numberOfComponents); @@ -321,7 +401,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif uint size = numberOfComponents * ExifDataTypes.GetSize(dataType); if (size > 4) { - uint newOffset = this.ConvertToUInt32(this.offsetBuffer); + uint newOffset = this.ConvertToUInt32(offsetBuffer); // Ensure that the new index does not overrun the data. if (newOffset > int.MaxValue || (newOffset + size) > this.data.Length) @@ -330,21 +410,85 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif return; } - this.RegisterExtLoader(newOffset, () => - { - byte[] dataBuffer = new byte[size]; - this.Seek(newOffset); - if (this.TryReadSpan(dataBuffer)) - { - object value = this.ConvertValue(dataType, dataBuffer, numberOfComponents); - this.Add(values, exifValue, value); - } - }); + this.BigValues.Add((newOffset, dataType, numberOfComponents, exifValue)); } else { - object value = this.ConvertValue(dataType, this.offsetBuffer, numberOfComponents); + object value = this.ConvertValue(dataType, offsetBuffer.Slice(0, (int)size), numberOfComponents > 1 || exifValue.IsArray); + this.Add(values, exifValue, value); + } + } + + private void ReadValue64(List values, Span offsetBuffer) + { + if ((this.data.Length - this.data.Position) < 20) + { + return; + } + + var tag = (ExifTagValue)this.ReadUInt16(); + ExifDataType dataType = EnumUtils.Parse(this.ReadUInt16(), ExifDataType.Unknown); + + ulong numberOfComponents = this.ReadUInt64(); + + this.TryReadSpan(offsetBuffer); + + if (dataType == ExifDataType.Unknown) + { + return; + } + if (numberOfComponents == 0) + { + numberOfComponents = 8 / ExifDataTypes.GetSize(dataType); + } + + // The StripOffsets, StripByteCounts, TileOffsets, and TileByteCounts tags are allowed to have the datatype TIFF_LONG8 in BigTIFF. + // Old datatypes TIFF_LONG, and TIFF_SHORT where allowed in the TIFF 6.0 specification, are still valid in BigTIFF, too. + // 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; + switch (tag) + { + case ExifTagValue.StripOffsets: + exifValue = new ExifLong8Array(ExifTagValue.StripOffsets); + break; + case ExifTagValue.StripByteCounts: + exifValue = new ExifLong8Array(ExifTagValue.StripByteCounts); + break; + case ExifTagValue.TileOffsets: + exifValue = new ExifLong8Array(ExifTagValue.TileOffsets); + break; + case ExifTagValue.TileByteCounts: + exifValue = new ExifLong8Array(ExifTagValue.TileByteCounts); + break; + default: + exifValue = ExifValues.Create(tag) ?? ExifValues.Create(tag, dataType, numberOfComponents); + break; + } + + if (exifValue is null) + { + this.AddInvalidTag(new UnkownExifTag(tag)); + return; + } + + ulong size = numberOfComponents * ExifDataTypes.GetSize(dataType); + if (size > 8) + { + ulong newOffset = this.ConvertToUInt64(offsetBuffer); + if (newOffset > ulong.MaxValue || newOffset > ((ulong)this.data.Length - size)) + { + this.AddInvalidTag(new UnkownExifTag(tag)); + return; + } + + this.BigValues.Add((newOffset, dataType, numberOfComponents, exifValue)); + } + else + { + object value = this.ConvertValue(dataType, offsetBuffer.Slice(0, (int)size), numberOfComponents > 1 || exifValue.IsArray); this.Add(values, exifValue, value); } } @@ -368,11 +512,11 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif if (exif.Tag == ExifTag.SubIFDOffset) { - this.exifOffset = (uint)value; + this.AddSubIfd(value); } else if (exif.Tag == ExifTag.GPSIFDOffset) { - this.gpsOffset = (uint)value; + this.AddSubIfd(value); } else { @@ -383,8 +527,11 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif private void AddInvalidTag(ExifTag tag) => (this.invalidTags ??= new List()).Add(tag); - private void Seek(long pos) - => this.data.Seek(pos, SeekOrigin.Begin); + private void AddSubIfd(object val) + => (this.subIfds ??= new List()).Add(Convert.ToUInt64(val)); + + private void Seek(ulong pos) + => this.data.Seek((long)pos, SeekOrigin.Begin); private bool TryReadSpan(Span span) { @@ -398,6 +545,11 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif return read == length; } + protected ulong ReadUInt64() => + this.TryReadSpan(this.buf8) + ? this.ConvertToUInt64(this.buf8) + : default; + // Known as Long in Exif Specification. protected uint ReadUInt32() => this.TryReadSpan(this.buf4) @@ -408,6 +560,30 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif ? this.ConvertToShort(this.buf2) : default; + private long ConvertToInt64(ReadOnlySpan buffer) + { + if (buffer.Length < 8) + { + return default; + } + + return this.IsBigEndian + ? BinaryPrimitives.ReadInt64BigEndian(buffer) + : BinaryPrimitives.ReadInt64LittleEndian(buffer); + } + + private ulong ConvertToUInt64(ReadOnlySpan buffer) + { + if (buffer.Length < 8) + { + return default; + } + + return this.IsBigEndian + ? BinaryPrimitives.ReadUInt64BigEndian(buffer) + : BinaryPrimitives.ReadUInt64LittleEndian(buffer); + } + private double ConvertToDouble(ReadOnlySpan buffer) { if (buffer.Length < 8) diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifUcs2StringHelpers.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifUcs2StringHelpers.cs new file mode 100644 index 0000000000..ccc1c80ade --- /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 e7a01b070f..a14539bca2 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 { @@ -46,23 +45,17 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif public byte[] GetData() { const uint startIndex = 0; - uint length; IExifValue exifOffset = GetOffsetValue(this.ifdValues, this.exifValues, ExifTag.SubIFDOffset); IExifValue gpsOffset = GetOffsetValue(this.ifdValues, this.gpsValues, ExifTag.GPSIFDOffset); - if (this.ifdValues.Count == 0 && this.exifValues.Count == 0 && this.gpsValues.Count == 0) - { - return Array.Empty(); - } - - uint ifdLength = this.GetLength(this.ifdValues) + 4U; + uint ifdLength = this.GetLength(this.ifdValues); uint exifLength = this.GetLength(this.exifValues); uint gpsLength = this.GetLength(this.gpsValues); - length = ifdLength + exifLength + gpsLength; + uint length = ifdLength + exifLength + gpsLength; - if (length == 4U) + if (length == 0) { return Array.Empty(); } @@ -70,9 +63,10 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif // two bytes for the byte Order marker 'II' or 'MM', followed by the number 42 (0x2A) and a 0, making 4 bytes total length += (uint)ExifConstants.LittleEndianByteOrderMarker.Length; - length += 4 + 2; + // first IFD offset + length += 4; - var result = new byte[length]; + byte[] result = new byte[length]; int i = 0; @@ -80,15 +74,13 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif ExifConstants.LittleEndianByteOrderMarker.CopyTo(result.AsSpan(start: i)); i += ExifConstants.LittleEndianByteOrderMarker.Length; - uint ifdOffset = ((uint)i - startIndex) + 4U; - uint thumbnailOffset = ifdOffset + ifdLength + exifLength + gpsLength; + uint ifdOffset = (uint)i - startIndex + 4U; exifOffset?.TrySetValue(ifdOffset + ifdLength); gpsOffset?.TrySetValue(ifdOffset + ifdLength + exifLength); i = WriteUInt32(ifdOffset, result, i); i = this.WriteHeaders(this.ifdValues, result, i); - i = WriteUInt32(thumbnailOffset, result, i); i = this.WriteData(startIndex, this.ifdValues, result, i); if (exifLength > 0) @@ -103,8 +95,6 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif i = this.WriteData(startIndex, this.gpsValues, result, i); } - WriteUInt16(0, result, i); - return result; } @@ -150,6 +140,20 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif return offset + 4; } + private static int WriteInt64(long value, Span destination, int offset) + { + BinaryPrimitives.WriteInt64LittleEndian(destination.Slice(offset, 8), value); + + return offset + 8; + } + + private static int WriteUInt64(ulong value, Span destination, int offset) + { + BinaryPrimitives.WriteUInt64LittleEndian(destination.Slice(offset, 8), value); + + return offset + 8; + } + private static int WriteInt32(int value, Span destination, int offset) { BinaryPrimitives.WriteInt32LittleEndian(destination.Slice(offset, 4), value); @@ -249,7 +253,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { uint valueLength = GetLength(value); - length += 2 + 2 + 4 + 4; + length += 12; if (valueLength > 4) { @@ -257,6 +261,9 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif } } + // next IFD offset + length += 4; + return length; } @@ -266,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) @@ -281,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()) { @@ -347,6 +359,9 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif newOffset += 4; } + // next IFD offset + newOffset = WriteUInt32(0, destination, newOffset); + return newOffset; } @@ -367,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: @@ -390,6 +405,10 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif } return WriteUInt32((uint)value, destination, offset); + case ExifDataType.Long8: + return WriteUInt64((ulong)value, destination, offset); + case ExifDataType.SignedLong8: + return WriteInt64((long)value, destination, offset); case ExifDataType.Rational: WriteRational(destination.Slice(offset, 8), (Rational)value); return offset + 8; @@ -410,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 fdde66c513..964fb6e948 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 0000000000..335098a435 --- /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.LongArray.cs b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs index ac4b0a1bf8..390599b730 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs @@ -65,5 +65,10 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif /// Gets the TimeZoneOffset exif tag. /// public static ExifTag TimeZoneOffset { get; } = new ExifTag(ExifTagValue.TimeZoneOffset); + + /// + /// Gets the offset to child IFDs exif tag. + /// + public static ExifTag SubIFDs { get; } = new ExifTag(ExifTagValue.SubIFDs); } } 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 0000000000..a6911d76d7 --- /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 1d9af6adce..58886f4036 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 0000000000..e9cd27427c --- /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 0000000000..ba9fca5c8f --- /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/Metadata/Profiles/Exif/Values/ExifLong8.cs b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifLong8.cs new file mode 100644 index 0000000000..a4a1dd3dfb --- /dev/null +++ b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifLong8.cs @@ -0,0 +1,66 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Globalization; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif +{ + internal sealed class ExifLong8 : ExifValue + { + public ExifLong8(ExifTag tag) + : base(tag) + { + } + + public ExifLong8(ExifTagValue tag) + : base(tag) + { + } + + private ExifLong8(ExifLong8 value) + : base(value) + { + } + + public override ExifDataType DataType => ExifDataType.Long8; + + protected override string StringValue => this.Value.ToString(CultureInfo.InvariantCulture); + + public override bool TrySetValue(object value) + { + if (base.TrySetValue(value)) + { + return true; + } + + switch (value) + { + case int intValue: + if (intValue >= uint.MinValue) + { + this.Value = (uint)intValue; + return true; + } + + return false; + case uint uintValue: + this.Value = uintValue; + + return true; + case long intValue: + if (intValue >= 0) + { + this.Value = (ulong)intValue; + return true; + } + + return false; + default: + + return false; + } + } + + public override IExifValue DeepClone() => new ExifLong8(this); + } +} diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifLong8Array.cs b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifLong8Array.cs new file mode 100644 index 0000000000..eced4e3de0 --- /dev/null +++ b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifLong8Array.cs @@ -0,0 +1,171 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif +{ + internal sealed class ExifLong8Array : ExifArrayValue + { + public ExifLong8Array(ExifTagValue tag) + : base(tag) + { + } + + private ExifLong8Array(ExifLong8Array value) + : base(value) + { + } + + public override ExifDataType DataType + { + get + { + if (this.Value is not null) + { + foreach (ulong value in this.Value) + { + if (value > uint.MaxValue) + { + return ExifDataType.Long8; + } + } + } + + return ExifDataType.Long; + } + } + + public override bool TrySetValue(object value) + { + if (base.TrySetValue(value)) + { + return true; + } + + switch (value) + { + case int val: + return this.SetSingle((ulong)Numerics.Clamp(val, 0, int.MaxValue)); + + case uint val: + return this.SetSingle((ulong)val); + + case short val: + return this.SetSingle((ulong)Numerics.Clamp(val, 0, short.MaxValue)); + + case ushort val: + return this.SetSingle((ulong)val); + + case long val: + return this.SetSingle((ulong)Numerics.Clamp(val, 0, long.MaxValue)); + + case long[] array: + { + if (value.GetType() == typeof(ulong[])) + { + return this.SetArray((ulong[])value); + } + + return this.SetArray(array); + } + + case int[] array: + { + if (value.GetType() == typeof(uint[])) + { + return this.SetArray((uint[])value); + } + + return this.SetArray(array); + } + + case short[] array: + { + if (value.GetType() == typeof(ushort[])) + { + return this.SetArray((ushort[])value); + } + + return this.SetArray(array); + } + } + + return false; + } + + public override IExifValue DeepClone() => new ExifLong8Array(this); + + private bool SetSingle(ulong value) + { + this.Value = new[] { value }; + return true; + } + + private bool SetArray(long[] values) + { + var numbers = new ulong[values.Length]; + for (int i = 0; i < values.Length; i++) + { + numbers[i] = (ulong)(values[i] < 0 ? 0 : values[i]); + } + + this.Value = numbers; + return true; + } + + private bool SetArray(ulong[] values) + { + this.Value = values; + return true; + } + + private bool SetArray(int[] values) + { + var numbers = new ulong[values.Length]; + for (int i = 0; i < values.Length; i++) + { + numbers[i] = (ulong)Numerics.Clamp(values[i], 0, int.MaxValue); + } + + this.Value = numbers; + return true; + } + + private bool SetArray(uint[] values) + { + var numbers = new ulong[values.Length]; + for (int i = 0; i < values.Length; i++) + { + numbers[i] = (ulong)values[i]; + } + + this.Value = numbers; + return true; + } + + private bool SetArray(short[] values) + { + var numbers = new ulong[values.Length]; + for (int i = 0; i < values.Length; i++) + { + numbers[i] = (ulong)Numerics.Clamp(values[i], 0, short.MaxValue); + } + + this.Value = numbers; + return true; + } + + private bool SetArray(ushort[] values) + { + var numbers = new ulong[values.Length]; + for (int i = 0; i < values.Length; i++) + { + numbers[i] = (ulong)values[i]; + } + + this.Value = numbers; + return true; + } + } +} diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifNumberArray.cs b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifNumberArray.cs index 2d3a93aed3..bf9c2cf9a9 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifNumberArray.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifNumberArray.cs @@ -19,16 +19,14 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { get { - if (this.Value is null) + if (this.Value is not null) { - return ExifDataType.Short; - } - - for (int i = 0; i < this.Value.Length; i++) - { - if (this.Value[i] > ushort.MaxValue) + foreach (Number value in this.Value) { - return ExifDataType.Long; + if (value > ushort.MaxValue) + { + return ExifDataType.Long; + } } } @@ -54,13 +52,25 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif case ushort val: return this.SetSingle(val); case int[] array: + { + // workaround for inconsistent covariance of value-typed arrays + if (value.GetType() == typeof(uint[])) + { + return this.SetArray((uint[])value); + } + return this.SetArray(array); - case uint[] array: - return this.SetArray(array); + } + case short[] array: + { + if (value.GetType() == typeof(ushort[])) + { + return this.SetArray((ushort[])value); + } + return this.SetArray(array); - case ushort[] array: - return this.SetArray(array); + } } return false; 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 a8ba5a55c0..b68390ae01 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/ExifSignedLong8.cs b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedLong8.cs new file mode 100644 index 0000000000..8362dcf2cf --- /dev/null +++ b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedLong8.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Globalization; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif +{ + internal sealed class ExifSignedLong8 : ExifValue + { + public ExifSignedLong8(ExifTagValue tag) + : base(tag) + { + } + + private ExifSignedLong8(ExifSignedLong8 value) + : base(value) + { + } + + public override ExifDataType DataType => ExifDataType.SignedLong8; + + protected override string StringValue => this.Value.ToString(CultureInfo.InvariantCulture); + + public override IExifValue DeepClone() => new ExifSignedLong8(this); + } +} diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedLong8Array.cs b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedLong8Array.cs new file mode 100644 index 0000000000..34ce20c8ff --- /dev/null +++ b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedLong8Array.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif +{ + internal sealed class ExifSignedLong8Array : ExifArrayValue + { + public ExifSignedLong8Array(ExifTagValue tag) + : base(tag) + { + } + + private ExifSignedLong8Array(ExifSignedLong8Array value) + : base(value) + { + } + + public override ExifDataType DataType => ExifDataType.SignedLong8; + + public override IExifValue DeepClone() => new ExifSignedLong8Array(this); + } +} 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 0000000000..42637925c7 --- /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/ExifValues.cs b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs index af1eee2dc8..fa5cf9b2fa 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs @@ -9,25 +9,42 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif public static ExifValue Create(ExifTag tag) => (ExifValue)CreateValue((ExifTagValue)(ushort)tag); - public static ExifValue Create(ExifTagValue tag, ExifDataType dataType, uint numberOfComponents) => Create(tag, dataType, numberOfComponents != 1); + public static ExifValue Create(ExifTagValue tag, ExifDataType dataType, ulong numberOfComponents) => Create(tag, dataType, numberOfComponents != 1); public static ExifValue Create(ExifTagValue tag, ExifDataType dataType, bool isArray) { 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.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.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; } } @@ -35,274 +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.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/Metadata/Profiles/IPTC/IptcProfile.cs b/src/ImageSharp/Metadata/Profiles/IPTC/IptcProfile.cs index e31842c537..610e52d1ca 100644 --- a/src/ImageSharp/Metadata/Profiles/IPTC/IptcProfile.cs +++ b/src/ImageSharp/Metadata/Profiles/IPTC/IptcProfile.cs @@ -16,6 +16,10 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc { private Collection values; + private const byte IptcTagMarkerByte = 0x1c; + + private const uint MaxStandardDataTagSize = 0x7FFF; + /// /// Initializes a new instance of the class. /// @@ -78,7 +82,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc } /// - public IptcProfile DeepClone() => new IptcProfile(this); + public IptcProfile DeepClone() => new(this); /// /// Returns all values with the specified tag. @@ -207,7 +211,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc throw new ArgumentException("iptc tag is not a time or date type"); } - var formattedDate = tag.IsDate() + string formattedDate = tag.IsDate() ? dateTimeOffset.ToString("yyyyMMdd", System.Globalization.CultureInfo.InvariantCulture) : dateTimeOffset.ToString("HHmmsszzzz", System.Globalization.CultureInfo.InvariantCulture) .Replace(":", string.Empty); @@ -231,7 +235,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc /// public void UpdateData() { - var length = 0; + int length = 0; foreach (IptcValue value in this.Values) { length += value.Length + 5; @@ -242,7 +246,24 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc int i = 0; foreach (IptcValue value in this.Values) { - this.Data[i++] = 28; + // Standard DataSet Tag + // +-----------+----------------+---------------------------------------------------------------------------------+ + // | Octet Pos | Name | Description | + // +==========-+================+=================================================================================+ + // | 1 | Tag Marker | Is the tag marker that initiates the start of a DataSet 0x1c. | + // +-----------+----------------+---------------------------------------------------------------------------------+ + // | 2 | Record Number | Octet 2 is the binary representation of the record number. Note that the | + // | | | envelope record number is always 1, and that the application records are | + // | | | numbered 2 through 6, the pre-object descriptor record is 7, the object record | + // | | | is 8, and the post - object descriptor record is 9. | + // +-----------+----------------+---------------------------------------------------------------------------------+ + // | 3 | DataSet Number | Octet 3 is the binary representation of the DataSet number. | + // +-----------+----------------+---------------------------------------------------------------------------------+ + // | 4 and 5 | Data Field | Octets 4 and 5, taken together, are the binary count of the number of octets in | + // | | Octet Count | the following data field(32767 or fewer octets). Note that the value of bit 7 of| + // | | | octet 4(most significant bit) always will be 0. | + // +-----------+----------------+---------------------------------------------------------------------------------+ + this.Data[i++] = IptcTagMarkerByte; this.Data[i++] = 2; this.Data[i++] = (byte)value.Tag; this.Data[i++] = (byte)(value.Length >> 8); @@ -264,34 +285,36 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc this.values = new Collection(); - if (this.Data == null || this.Data[0] != 0x1c) + if (this.Data == null || this.Data[0] != IptcTagMarkerByte) { return; } - int i = 0; - while (i + 4 < this.Data.Length) + int offset = 0; + while (offset < this.Data.Length - 4) { - if (this.Data[i++] != 28) + bool isValidTagMarker = this.Data[offset++] == IptcTagMarkerByte; + byte recordNumber = this.Data[offset++]; + bool isValidRecordNumber = recordNumber is >= 1 and <= 9; + var tag = (IptcTag)this.Data[offset++]; + bool isValidEntry = isValidTagMarker && isValidRecordNumber; + + uint byteCount = BinaryPrimitives.ReadUInt16BigEndian(this.Data.AsSpan(offset, 2)); + offset += 2; + if (byteCount > MaxStandardDataTagSize) { - continue; + // Extended data set tag's are not supported. + break; } - i++; - - var tag = (IptcTag)this.Data[i++]; - - int count = BinaryPrimitives.ReadInt16BigEndian(this.Data.AsSpan(i, 2)); - i += 2; - - var iptcData = new byte[count]; - if ((count > 0) && (i + count <= this.Data.Length)) + if (isValidEntry && byteCount > 0 && (offset <= this.Data.Length - byteCount)) { - Buffer.BlockCopy(this.Data, i, iptcData, 0, count); + var iptcData = new byte[byteCount]; + Buffer.BlockCopy(this.Data, offset, iptcData, 0, (int)byteCount); this.values.Add(new IptcValue(tag, iptcData, false)); } - i += count; + offset += (int)byteCount; } } } diff --git a/src/ImageSharp/Metadata/Profiles/IPTC/IptcTagExtensions.cs b/src/ImageSharp/Metadata/Profiles/IPTC/IptcTagExtensions.cs index b670591df7..4b18add74e 100644 --- a/src/ImageSharp/Metadata/Profiles/IPTC/IptcTagExtensions.cs +++ b/src/ImageSharp/Metadata/Profiles/IPTC/IptcTagExtensions.cs @@ -13,60 +13,57 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc /// /// The tag to check the max length for. /// The maximum length. - public static int MaxLength(this IptcTag tag) + public static int MaxLength(this IptcTag tag) => tag switch { - return tag switch - { - IptcTag.RecordVersion => 2, - IptcTag.ObjectType => 67, - IptcTag.ObjectAttribute => 68, - IptcTag.Name => 64, - IptcTag.EditStatus => 64, - IptcTag.EditorialUpdate => 2, - IptcTag.Urgency => 1, - IptcTag.SubjectReference => 236, - IptcTag.Category => 3, - IptcTag.SupplementalCategories => 32, - IptcTag.FixtureIdentifier => 32, - IptcTag.Keywords => 64, - IptcTag.LocationCode => 3, - IptcTag.LocationName => 64, - IptcTag.ReleaseDate => 8, - IptcTag.ReleaseTime => 11, - IptcTag.ExpirationDate => 8, - IptcTag.ExpirationTime => 11, - IptcTag.SpecialInstructions => 256, - IptcTag.ActionAdvised => 2, - IptcTag.ReferenceService => 10, - IptcTag.ReferenceDate => 8, - IptcTag.ReferenceNumber => 8, - IptcTag.CreatedDate => 8, - IptcTag.CreatedTime => 11, - IptcTag.DigitalCreationDate => 8, - IptcTag.DigitalCreationTime => 11, - IptcTag.OriginatingProgram => 32, - IptcTag.ProgramVersion => 10, - IptcTag.ObjectCycle => 1, - IptcTag.Byline => 32, - IptcTag.BylineTitle => 32, - IptcTag.City => 32, - IptcTag.SubLocation => 32, - IptcTag.ProvinceState => 32, - IptcTag.CountryCode => 3, - IptcTag.Country => 64, - IptcTag.OriginalTransmissionReference => 32, - IptcTag.Headline => 256, - IptcTag.Credit => 32, - IptcTag.Source => 32, - IptcTag.CopyrightNotice => 128, - IptcTag.Contact => 128, - IptcTag.Caption => 2000, - IptcTag.CaptionWriter => 32, - IptcTag.ImageType => 2, - IptcTag.ImageOrientation => 1, - _ => 256 - }; - } + IptcTag.RecordVersion => 2, + IptcTag.ObjectType => 67, + IptcTag.ObjectAttribute => 68, + IptcTag.Name => 64, + IptcTag.EditStatus => 64, + IptcTag.EditorialUpdate => 2, + IptcTag.Urgency => 1, + IptcTag.SubjectReference => 236, + IptcTag.Category => 3, + IptcTag.SupplementalCategories => 32, + IptcTag.FixtureIdentifier => 32, + IptcTag.Keywords => 64, + IptcTag.LocationCode => 3, + IptcTag.LocationName => 64, + IptcTag.ReleaseDate => 8, + IptcTag.ReleaseTime => 11, + IptcTag.ExpirationDate => 8, + IptcTag.ExpirationTime => 11, + IptcTag.SpecialInstructions => 256, + IptcTag.ActionAdvised => 2, + IptcTag.ReferenceService => 10, + IptcTag.ReferenceDate => 8, + IptcTag.ReferenceNumber => 8, + IptcTag.CreatedDate => 8, + IptcTag.CreatedTime => 11, + IptcTag.DigitalCreationDate => 8, + IptcTag.DigitalCreationTime => 11, + IptcTag.OriginatingProgram => 32, + IptcTag.ProgramVersion => 10, + IptcTag.ObjectCycle => 1, + IptcTag.Byline => 32, + IptcTag.BylineTitle => 32, + IptcTag.City => 32, + IptcTag.SubLocation => 32, + IptcTag.ProvinceState => 32, + IptcTag.CountryCode => 3, + IptcTag.Country => 64, + IptcTag.OriginalTransmissionReference => 32, + IptcTag.Headline => 256, + IptcTag.Credit => 32, + IptcTag.Source => 32, + IptcTag.CopyrightNotice => 128, + IptcTag.Contact => 128, + IptcTag.Caption => 2000, + IptcTag.CaptionWriter => 32, + IptcTag.ImageType => 2, + IptcTag.ImageOrientation => 1, + _ => 256 + }; /// /// Determines if the given tag can be repeated according to the specification. diff --git a/src/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs b/src/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs index 9e409ca064..5ba81bea70 100644 --- a/src/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs +++ b/src/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs @@ -101,7 +101,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc byte[] valueBytes; if (this.Strict && value.Length > maxLength) { - var cappedValue = value.Substring(0, maxLength); + string cappedValue = value.Substring(0, maxLength); valueBytes = this.encoding.GetBytes(cappedValue); // It is still possible that the bytes of the string exceed the limit. diff --git a/src/ImageSharp/Metadata/Profiles/XMP/XmpProfile.cs b/src/ImageSharp/Metadata/Profiles/XMP/XmpProfile.cs new file mode 100644 index 0000000000..8fba243ce2 --- /dev/null +++ b/src/ImageSharp/Metadata/Profiles/XMP/XmpProfile.cs @@ -0,0 +1,89 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; +using System.Text; +using System.Xml.Linq; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Xmp +{ + /// + /// Represents an XMP profile, providing access to the raw XML. + /// See for the full specification. + /// + public sealed class XmpProfile : IDeepCloneable + { + /// + /// Initializes a new instance of the class. + /// + public XmpProfile() + : this((byte[])null) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The UTF8 encoded byte array to read the XMP profile from. + public XmpProfile(byte[] data) => this.Data = data; + + /// + /// Initializes a new instance of the class + /// by making a copy from another XMP profile. + /// + /// The other XMP profile, from which the clone should be made from. + private XmpProfile(XmpProfile other) + { + Guard.NotNull(other, nameof(other)); + + this.Data = other.Data; + } + + /// + /// Gets the XMP raw data byte array. + /// + internal byte[] Data { get; private set; } + + /// + /// Gets the raw XML document containing the XMP profile. + /// + /// The + public XDocument GetDocument() + { + byte[] byteArray = this.Data; + if (byteArray is null) + { + return null; + } + + // Strip leading whitespace, as the XmlReader doesn't like them. + int count = byteArray.Length; + for (int i = count - 1; i > 0; i--) + { + if (byteArray[i] is 0 or 0x0f) + { + count--; + } + } + + using var stream = new MemoryStream(byteArray, 0, count); + using var reader = new StreamReader(stream, Encoding.UTF8); + return XDocument.Load(reader); + } + + /// + /// Convert the content of this into a byte array. + /// + /// The + public byte[] ToByteArray() + { + byte[] result = new byte[this.Data.Length]; + this.Data.AsSpan().CopyTo(result); + return result; + } + + /// + public XmpProfile DeepClone() => new(this); + } +} diff --git a/src/ImageSharp/Processing/Processors/CloningImageProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/CloningImageProcessor{TPixel}.cs index 2a41329a63..3e420ca03b 100644 --- a/src/ImageSharp/Processing/Processors/CloningImageProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/CloningImageProcessor{TPixel}.cs @@ -46,38 +46,25 @@ namespace SixLabors.ImageSharp.Processing.Processors /// Image ICloningImageProcessor.CloneAndExecute() { - try - { - Image clone = this.CreateTarget(); - this.CheckFrameCount(this.Source, clone); + Image clone = this.CreateTarget(); + this.CheckFrameCount(this.Source, clone); - Configuration configuration = this.Configuration; - this.BeforeImageApply(clone); + Configuration configuration = this.Configuration; + this.BeforeImageApply(clone); - for (int i = 0; i < this.Source.Frames.Count; i++) - { - ImageFrame sourceFrame = this.Source.Frames[i]; - ImageFrame clonedFrame = clone.Frames[i]; + for (int i = 0; i < this.Source.Frames.Count; i++) + { + ImageFrame sourceFrame = this.Source.Frames[i]; + ImageFrame clonedFrame = clone.Frames[i]; - this.BeforeFrameApply(sourceFrame, clonedFrame); - this.OnFrameApply(sourceFrame, clonedFrame); - this.AfterFrameApply(sourceFrame, clonedFrame); - } + this.BeforeFrameApply(sourceFrame, clonedFrame); + this.OnFrameApply(sourceFrame, clonedFrame); + this.AfterFrameApply(sourceFrame, clonedFrame); + } - this.AfterImageApply(clone); + this.AfterImageApply(clone); - return clone; - } -#if DEBUG - catch (Exception) - { - throw; -#else - catch (Exception ex) - { - throw new ImageProcessingException($"An error occurred when processing the image using {this.GetType().Name}. See the inner exception for more detail.", ex); -#endif - } + return clone; } /// diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor.cs index 77ba9582d6..75f82a25a6 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor.cs @@ -21,6 +21,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms Guard.NotNull(sampler, nameof(sampler)); Guard.MustBeValueType(sampler, nameof(sampler)); + if (TransformUtils.IsDegenerate(matrix)) + { + throw new DegenerateTransformException("Matrix is degenerate. Check input values."); + } + this.Sampler = sampler; this.TransformMatrix = matrix; this.DestinationSize = targetDimensions; diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs index 640527fe7c..f6fadca33e 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs @@ -61,10 +61,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms Matrix3x2 matrix = this.transformMatrix; // Handle transforms that result in output identical to the original. - if (matrix.Equals(default) || matrix.Equals(Matrix3x2.Identity)) + // Degenerate matrices are already handled in the upstream definition. + if (matrix.Equals(Matrix3x2.Identity)) { // The clone will be blank here copy all the pixel data over - source.GetPixelMemoryGroup().CopyTo(destination.GetPixelMemoryGroup()); + var interest = Rectangle.Intersect(this.SourceRectangle, destination.Bounds()); + Buffer2DRegion sourceBuffer = source.PixelBuffer.GetRegion(interest); + Buffer2DRegion destbuffer = destination.PixelBuffer.GetRegion(interest); + for (int y = 0; y < sourceBuffer.Height; y++) + { + sourceBuffer.DangerousGetRowSpan(y).CopyTo(destbuffer.DangerousGetRowSpan(y)); + } + return; } @@ -73,7 +81,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms if (sampler is NearestNeighborResampler) { - var nnOperation = new NNAffineOperation(source.PixelBuffer, destination.PixelBuffer, matrix); + var nnOperation = new NNAffineOperation( + source.PixelBuffer, + Rectangle.Intersect(this.SourceRectangle, source.Bounds()), + destination.PixelBuffer, + matrix); + ParallelRowIterator.IterateRows( configuration, destination.Bounds(), @@ -85,6 +98,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms var operation = new AffineOperation( configuration, source.PixelBuffer, + Rectangle.Intersect(this.SourceRectangle, source.Bounds()), destination.PixelBuffer, in sampler, matrix); @@ -105,12 +119,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms [MethodImpl(InliningOptions.ShortMethod)] public NNAffineOperation( Buffer2D source, + Rectangle bounds, Buffer2D destination, Matrix3x2 matrix) { this.source = source; + this.bounds = bounds; this.destination = destination; - this.bounds = source.Bounds(); this.matrix = matrix; } @@ -138,6 +153,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms { private readonly Configuration configuration; private readonly Buffer2D source; + private readonly Rectangle bounds; private readonly Buffer2D destination; private readonly TResampler sampler; private readonly Matrix3x2 matrix; @@ -148,12 +164,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms public AffineOperation( Configuration configuration, Buffer2D source, + Rectangle bounds, Buffer2D destination, in TResampler sampler, Matrix3x2 matrix) { this.configuration = configuration; this.source = source; + this.bounds = bounds; this.destination = destination; this.sampler = sampler; this.matrix = matrix; @@ -182,8 +200,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms TResampler sampler = this.sampler; float yRadius = this.yRadius; float xRadius = this.xRadius; - int maxY = this.source.Height - 1; - int maxX = this.source.Width - 1; + int minY = this.bounds.Y; + int maxY = this.bounds.Bottom - 1; + int minX = this.bounds.X; + int maxX = this.bounds.Right - 1; for (int y = rows.Min; y < rows.Max; y++) { @@ -200,10 +220,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms float pY = point.Y; float pX = point.X; - int top = LinearTransformUtility.GetRangeStart(yRadius, pY, maxY); - int bottom = LinearTransformUtility.GetRangeEnd(yRadius, pY, maxY); - int left = LinearTransformUtility.GetRangeStart(xRadius, pX, maxX); - int right = LinearTransformUtility.GetRangeEnd(xRadius, pX, maxX); + int top = LinearTransformUtility.GetRangeStart(yRadius, pY, minY, maxY); + int bottom = LinearTransformUtility.GetRangeEnd(yRadius, pY, minY, maxY); + int left = LinearTransformUtility.GetRangeStart(xRadius, pX, minX, maxX); + int right = LinearTransformUtility.GetRangeEnd(xRadius, pX, minX, maxX); if (bottom == top || right == left) { @@ -245,8 +265,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms TResampler sampler = this.sampler; float yRadius = this.yRadius; float xRadius = this.xRadius; - int maxY = this.source.Height - 1; - int maxX = this.source.Width - 1; + int minY = this.bounds.Y; + int maxY = this.bounds.Bottom - 1; + int minX = this.bounds.X; + int maxX = this.bounds.Right - 1; for (int y = rows.Min; y < rows.Max; y++) { @@ -263,10 +285,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms float pY = point.Y; float pX = point.X; - int top = LinearTransformUtility.GetRangeStart(yRadius, pY, maxY); - int bottom = LinearTransformUtility.GetRangeEnd(yRadius, pY, maxY); - int left = LinearTransformUtility.GetRangeStart(xRadius, pX, maxX); - int right = LinearTransformUtility.GetRangeEnd(xRadius, pX, maxX); + int top = LinearTransformUtility.GetRangeStart(yRadius, pY, minY, maxY); + int bottom = LinearTransformUtility.GetRangeEnd(yRadius, pY, minY, maxY); + int left = LinearTransformUtility.GetRangeStart(xRadius, pX, minX, maxX); + int right = LinearTransformUtility.GetRangeEnd(xRadius, pX, minX, maxX); if (bottom == top || right == left) { diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/AutoOrientProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/AutoOrientProcessor{TPixel}.cs index 6c5219b3a5..c9f9d4327a 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/LinearTransformUtility.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/LinearTransformUtility.cs index c6168b4619..fd0c7f23bd 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Linear/LinearTransformUtility.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/LinearTransformUtility.cs @@ -39,11 +39,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// /// The radius. /// The center position. + /// The min allowed amouunt. /// The max allowed amouunt. /// The . [MethodImpl(InliningOptions.ShortMethod)] - public static int GetRangeStart(float radius, float center, int max) - => Numerics.Clamp((int)MathF.Ceiling(center - radius), 0, max); + public static int GetRangeStart(float radius, float center, int min, int max) + => Numerics.Clamp((int)MathF.Ceiling(center - radius), min, max); /// /// Gets the end position (inclusive) for a sampling range given @@ -51,10 +52,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// /// The radius. /// The center position. + /// The min allowed amouunt. /// The max allowed amouunt. /// The . [MethodImpl(InliningOptions.ShortMethod)] - public static int GetRangeEnd(float radius, float center, int max) - => Numerics.Clamp((int)MathF.Floor(center + radius), 0, max); + public static int GetRangeEnd(float radius, float center, int min, int max) + => Numerics.Clamp((int)MathF.Floor(center + radius), min, max); } } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor.cs index 338489d3f3..5eb89fe8ae 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor.cs @@ -21,6 +21,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms Guard.NotNull(sampler, nameof(sampler)); Guard.MustBeValueType(sampler, nameof(sampler)); + if (TransformUtils.IsDegenerate(matrix)) + { + throw new DegenerateTransformException("Matrix is degenerate. Check input values."); + } + this.Sampler = sampler; this.TransformMatrix = matrix; this.DestinationSize = targetDimensions; diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs index cf6567629f..26b970d8b5 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs @@ -60,10 +60,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms Matrix4x4 matrix = this.transformMatrix; // Handle transforms that result in output identical to the original. - if (matrix.Equals(default) || matrix.Equals(Matrix4x4.Identity)) + // Degenerate matrices are already handled in the upstream definition. + if (matrix.Equals(Matrix4x4.Identity)) { // The clone will be blank here copy all the pixel data over - source.GetPixelMemoryGroup().CopyTo(destination.GetPixelMemoryGroup()); + var interest = Rectangle.Intersect(this.SourceRectangle, destination.Bounds()); + Buffer2DRegion sourceBuffer = source.PixelBuffer.GetRegion(interest); + Buffer2DRegion destbuffer = destination.PixelBuffer.GetRegion(interest); + for (int y = 0; y < sourceBuffer.Height; y++) + { + sourceBuffer.DangerousGetRowSpan(y).CopyTo(destbuffer.DangerousGetRowSpan(y)); + } + return; } @@ -72,7 +80,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms if (sampler is NearestNeighborResampler) { - var nnOperation = new NNProjectiveOperation(source.PixelBuffer, destination.PixelBuffer, matrix); + var nnOperation = new NNProjectiveOperation( + source.PixelBuffer, + Rectangle.Intersect(this.SourceRectangle, source.Bounds()), + destination.PixelBuffer, + matrix); + ParallelRowIterator.IterateRows( configuration, destination.Bounds(), @@ -84,6 +97,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms var operation = new ProjectiveOperation( configuration, source.PixelBuffer, + Rectangle.Intersect(this.SourceRectangle, source.Bounds()), destination.PixelBuffer, in sampler, matrix); @@ -104,12 +118,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms [MethodImpl(InliningOptions.ShortMethod)] public NNProjectiveOperation( Buffer2D source, + Rectangle bounds, Buffer2D destination, Matrix4x4 matrix) { this.source = source; + this.bounds = bounds; this.destination = destination; - this.bounds = source.Bounds(); this.matrix = matrix; } @@ -137,6 +152,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms { private readonly Configuration configuration; private readonly Buffer2D source; + private readonly Rectangle bounds; private readonly Buffer2D destination; private readonly TResampler sampler; private readonly Matrix4x4 matrix; @@ -147,18 +163,20 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms public ProjectiveOperation( Configuration configuration, Buffer2D source, + Rectangle bounds, Buffer2D destination, in TResampler sampler, Matrix4x4 matrix) { this.configuration = configuration; this.source = source; + this.bounds = bounds; this.destination = destination; this.sampler = sampler; this.matrix = matrix; - this.yRadius = LinearTransformUtility.GetSamplingRadius(in sampler, source.Height, destination.Height); - this.xRadius = LinearTransformUtility.GetSamplingRadius(in sampler, source.Width, destination.Width); + this.yRadius = LinearTransformUtility.GetSamplingRadius(in sampler, bounds.Height, destination.Height); + this.xRadius = LinearTransformUtility.GetSamplingRadius(in sampler, bounds.Width, destination.Width); } [MethodImpl(InliningOptions.ShortMethod)] @@ -181,8 +199,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms TResampler sampler = this.sampler; float yRadius = this.yRadius; float xRadius = this.xRadius; - int maxY = this.source.Height - 1; - int maxX = this.source.Width - 1; + int minY = this.bounds.Y; + int maxY = this.bounds.Bottom - 1; + int minX = this.bounds.X; + int maxX = this.bounds.Right - 1; for (int y = rows.Min; y < rows.Max; y++) { @@ -199,10 +219,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms float pY = point.Y; float pX = point.X; - int top = LinearTransformUtility.GetRangeStart(yRadius, pY, maxY); - int bottom = LinearTransformUtility.GetRangeEnd(yRadius, pY, maxY); - int left = LinearTransformUtility.GetRangeStart(xRadius, pX, maxX); - int right = LinearTransformUtility.GetRangeEnd(xRadius, pX, maxX); + int top = LinearTransformUtility.GetRangeStart(yRadius, pY, minY, maxY); + int bottom = LinearTransformUtility.GetRangeEnd(yRadius, pY, minY, maxY); + int left = LinearTransformUtility.GetRangeStart(xRadius, pX, minX, maxX); + int right = LinearTransformUtility.GetRangeEnd(xRadius, pX, minX, maxX); if (bottom <= top || right <= left) { @@ -244,8 +264,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms TResampler sampler = this.sampler; float yRadius = this.yRadius; float xRadius = this.xRadius; - int maxY = this.source.Height - 1; - int maxX = this.source.Width - 1; + int minY = this.bounds.Y; + int maxY = this.bounds.Bottom - 1; + int minX = this.bounds.X; + int maxX = this.bounds.Right - 1; for (int y = rows.Min; y < rows.Max; y++) { @@ -262,10 +284,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms float pY = point.Y; float pX = point.X; - int top = LinearTransformUtility.GetRangeStart(yRadius, pY, maxY); - int bottom = LinearTransformUtility.GetRangeEnd(yRadius, pY, maxY); - int left = LinearTransformUtility.GetRangeStart(xRadius, pX, maxX); - int right = LinearTransformUtility.GetRangeEnd(xRadius, pX, maxX); + int top = LinearTransformUtility.GetRangeStart(yRadius, pY, minY, maxY); + int bottom = LinearTransformUtility.GetRangeEnd(yRadius, pY, minY, maxY); + int left = LinearTransformUtility.GetRangeStart(xRadius, pX, minX, maxX); + int right = LinearTransformUtility.GetRangeEnd(xRadius, pX, minX, maxX); if (bottom <= top || right <= left) { diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs index 4e3a08c393..90cbf8bda0 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs @@ -88,7 +88,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms this.transposedFirstPassBuffer = configuration.MemoryAllocator.Allocate2D( this.workerHeight, destWidth, - AllocationOptions.Clean); + preferContiguosImageBuffers: true, + options: AllocationOptions.Clean); this.tempRowBuffer = configuration.MemoryAllocator.Allocate(this.sourceRectangle.Width); this.tempColumnBuffer = configuration.MemoryAllocator.Allocate(destWidth); @@ -171,7 +172,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms for (int y = calculationInterval.Min; y < calculationInterval.Max; y++) { - Span sourceRow = this.source.GetRowSpan(y); + Span sourceRow = this.source.DangerousGetRowSpan(y); PixelOperations.Instance.ToVector4( this.configuration, diff --git a/tests/Directory.Build.targets b/tests/Directory.Build.targets index ddceaff1fc..5258601897 100644 --- a/tests/Directory.Build.targets +++ b/tests/Directory.Build.targets @@ -33,6 +33,7 @@ + diff --git a/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs b/tests/ImageSharp.Benchmarks/Codecs/Jpeg/DecodeJpegParseStreamOnly.cs index 9db666c374..988c056608 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 }); + using 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(); } // 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.Tests/Formats/Gif/GifDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs index 6bf606ac90..c8ecdb717b 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs @@ -247,5 +247,17 @@ 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); + } } } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs index ae7e81254b..9576cbd3c8 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/Block8x8FTests.cs @@ -220,7 +220,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg // Reference implementation quantizes given block via division Block8x8 expected = default; - ReferenceImplementations.Quantize(ref source, ref expected, ref quant, ZigZag.ZigZagOrder); + ReferenceImplementations.Quantize(ref source, ref expected, ref quant, ZigZag.TransposingOrder); // Actual current implementation quantizes given block via multiplication // With quantization table reciprocal diff --git a/tests/ImageSharp.Tests/Formats/Jpg/DCTTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/DCTTests.cs index 85f30d28d7..9c467a1cc9 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/DCTTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/DCTTests.cs @@ -135,10 +135,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg FastFloatingPointDCT.AdjustToIDCT(ref dequantMatrix); srcBlock.MultiplyInPlace(ref dequantMatrix); + // testee // IDCT implementation tranforms blocks after transposition srcBlock.TransposeInplace(); - - // IDCT calculation FastFloatingPointDCT.TransformIDCT(ref srcBlock); float[] actualDest = srcBlock.ToArray(); @@ -149,12 +148,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg // 4 paths: // 1. AllowAll - call avx/fma implementation // 2. DisableFMA - call avx without fma implementation - // 3. DisableAvx - call sse Vector4 implementation - // 4. DisableHWIntrinsic - call scalar fallback implementation + // 3. DisableAvx - call sse implementation + // 4. DisableSIMD - call Vector4 fallback implementation FeatureTestRunner.RunWithHwIntrinsicsFeature( RunTest, seed, - HwIntrinsics.AllowAll | HwIntrinsics.DisableFMA | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + HwIntrinsics.AllowAll | HwIntrinsics.DisableFMA | HwIntrinsics.DisableAVX | HwIntrinsics.DisableSIMD); } // Forward transform @@ -180,7 +179,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg ReferenceImplementations.LLM_FloatingPoint_DCT.FDCT2D_llm(src, expectedDest, temp1, downscaleBy8: true); // testee + // Second transpose call is done by Quantize step + // Do this manually here just to be complient to the reference implementation FastFloatingPointDCT.TransformFDCT(ref block); + block.TransposeInplace(); // Part of the IDCT calculations is fused into the quantization step // We must multiply input block with adjusted no-quantization matrix @@ -197,12 +199,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg // 4 paths: // 1. AllowAll - call avx/fma implementation // 2. DisableFMA - call avx without fma implementation - // 3. DisableAvx - call sse Vector4 implementation - // 4. DisableHWIntrinsic - call scalar fallback implementation + // 3. DisableAvx - call Vector4 implementation + // 4. DisableSIMD - call scalar fallback implementation FeatureTestRunner.RunWithHwIntrinsicsFeature( RunTest, seed, - HwIntrinsics.AllowAll | HwIntrinsics.DisableFMA | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); + HwIntrinsics.AllowAll | HwIntrinsics.DisableFMA | HwIntrinsics.DisableAVX | HwIntrinsics.DisableSIMD); } } } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs index d82359e61a..70cbc3af72 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs @@ -10,58 +10,75 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg public partial class JpegDecoderTests { public static string[] BaselineTestJpegs = - { - TestImages.Jpeg.Baseline.Calliphora, - TestImages.Jpeg.Baseline.Cmyk, - TestImages.Jpeg.Baseline.Ycck, - TestImages.Jpeg.Baseline.Jpeg400, - TestImages.Jpeg.Baseline.Turtle420, - TestImages.Jpeg.Baseline.Testorig420, - TestImages.Jpeg.Baseline.Jpeg420Small, - TestImages.Jpeg.Issues.Fuzz.AccessViolationException922, - TestImages.Jpeg.Baseline.Jpeg444, - TestImages.Jpeg.Baseline.Jpeg422, - TestImages.Jpeg.Baseline.Bad.BadEOF, - TestImages.Jpeg.Baseline.MultiScanBaselineCMYK, - TestImages.Jpeg.Baseline.YcckSubsample1222, - TestImages.Jpeg.Baseline.Bad.BadRST, - TestImages.Jpeg.Issues.MultiHuffmanBaseline394, - TestImages.Jpeg.Issues.ExifDecodeOutOfRange694, - TestImages.Jpeg.Issues.InvalidEOI695, - TestImages.Jpeg.Issues.ExifResizeOutOfRange696, - TestImages.Jpeg.Issues.InvalidAPP0721, - TestImages.Jpeg.Issues.ExifGetString750Load, - TestImages.Jpeg.Issues.ExifGetString750Transform, - TestImages.Jpeg.Issues.BadSubSampling1076, + { + TestImages.Jpeg.Baseline.Calliphora, + TestImages.Jpeg.Baseline.Cmyk, + TestImages.Jpeg.Baseline.Ycck, + TestImages.Jpeg.Baseline.Jpeg400, + TestImages.Jpeg.Baseline.Turtle420, + TestImages.Jpeg.Baseline.Testorig420, + TestImages.Jpeg.Baseline.Jpeg420Small, + TestImages.Jpeg.Issues.Fuzz.AccessViolationException922, + TestImages.Jpeg.Baseline.Jpeg444, + TestImages.Jpeg.Baseline.Jpeg422, + TestImages.Jpeg.Baseline.Bad.BadEOF, + TestImages.Jpeg.Baseline.MultiScanBaselineCMYK, + TestImages.Jpeg.Baseline.YcckSubsample1222, + TestImages.Jpeg.Baseline.Bad.BadRST, + TestImages.Jpeg.Issues.MultiHuffmanBaseline394, + TestImages.Jpeg.Issues.ExifDecodeOutOfRange694, + TestImages.Jpeg.Issues.InvalidEOI695, + TestImages.Jpeg.Issues.ExifResizeOutOfRange696, + TestImages.Jpeg.Issues.InvalidAPP0721, + TestImages.Jpeg.Issues.ExifGetString750Load, + TestImages.Jpeg.Issues.ExifGetString750Transform, + TestImages.Jpeg.Issues.BadSubSampling1076, + + // LibJpeg can open this despite the invalid density units. + TestImages.Jpeg.Issues.Fuzz.ArgumentOutOfRangeException825B, - // LibJpeg can open this despite the invalid density units. - TestImages.Jpeg.Issues.Fuzz.ArgumentOutOfRangeException825B, + // LibJpeg can open this despite incorrect colorspace metadata. + TestImages.Jpeg.Issues.IncorrectColorspace855, - // LibJpeg can open this despite incorrect colorspace metadata. - TestImages.Jpeg.Issues.IncorrectColorspace855, + // High depth images + TestImages.Jpeg.Baseline.Testorig12bit, - // 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 = - { - TestImages.Jpeg.Progressive.Fb, - TestImages.Jpeg.Progressive.Progress, - TestImages.Jpeg.Progressive.Festzug, - TestImages.Jpeg.Progressive.Bad.BadEOF, - TestImages.Jpeg.Issues.BadCoeffsProgressive178, - TestImages.Jpeg.Issues.MissingFF00ProgressiveGirl159, - TestImages.Jpeg.Issues.MissingFF00ProgressiveBedroom159, - TestImages.Jpeg.Issues.BadZigZagProgressive385, - TestImages.Jpeg.Progressive.Bad.ExifUndefType, - TestImages.Jpeg.Issues.NoEoiProgressive517, - TestImages.Jpeg.Issues.BadRstProgressive518, - TestImages.Jpeg.Issues.DhtHasWrongLength624, - TestImages.Jpeg.Issues.OrderedInterleavedProgressive723A, - TestImages.Jpeg.Issues.OrderedInterleavedProgressive723B, - TestImages.Jpeg.Issues.OrderedInterleavedProgressive723C - }; + { + TestImages.Jpeg.Progressive.Fb, + TestImages.Jpeg.Progressive.Progress, + TestImages.Jpeg.Progressive.Festzug, + TestImages.Jpeg.Progressive.Bad.BadEOF, + TestImages.Jpeg.Issues.BadCoeffsProgressive178, + TestImages.Jpeg.Issues.MissingFF00ProgressiveGirl159, + TestImages.Jpeg.Issues.MissingFF00ProgressiveBedroom159, + TestImages.Jpeg.Issues.BadZigZagProgressive385, + TestImages.Jpeg.Progressive.Bad.ExifUndefType, + TestImages.Jpeg.Issues.NoEoiProgressive517, + TestImages.Jpeg.Issues.BadRstProgressive518, + TestImages.Jpeg.Issues.DhtHasWrongLength624, + TestImages.Jpeg.Issues.OrderedInterleavedProgressive723A, + TestImages.Jpeg.Issues.OrderedInterleavedProgressive723B, + TestImages.Jpeg.Issues.OrderedInterleavedProgressive723C + }; + + public static string[] UnsupportedTestJpegs = + { + // Invalid componentCount value (2 or > 4) + 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 + }; public static string[] UnrecoverableTestJpegs = { @@ -70,7 +87,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg TestImages.Jpeg.Issues.Fuzz.AccessViolationException798, TestImages.Jpeg.Issues.Fuzz.DivideByZeroException821, TestImages.Jpeg.Issues.Fuzz.DivideByZeroException822, - TestImages.Jpeg.Issues.Fuzz.NullReferenceException823, TestImages.Jpeg.Issues.Fuzz.IndexOutOfRangeException824A, TestImages.Jpeg.Issues.Fuzz.IndexOutOfRangeException824B, TestImages.Jpeg.Issues.Fuzz.IndexOutOfRangeException824D, @@ -91,28 +107,27 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg TestImages.Jpeg.Issues.Fuzz.IndexOutOfRangeException824C, }; - private static readonly Dictionary CustomToleranceValues = - new Dictionary - { - // Baseline: - [TestImages.Jpeg.Baseline.Calliphora] = 0.00002f / 100, - [TestImages.Jpeg.Baseline.Bad.BadEOF] = 0.38f / 100, - [TestImages.Jpeg.Baseline.Bad.BadRST] = 0.0589f / 100, + private static readonly Dictionary CustomToleranceValues = new() + { + // Baseline: + [TestImages.Jpeg.Baseline.Calliphora] = 0.00002f / 100, + [TestImages.Jpeg.Baseline.Bad.BadEOF] = 0.38f / 100, + [TestImages.Jpeg.Baseline.Bad.BadRST] = 0.0589f / 100, - [TestImages.Jpeg.Baseline.Jpeg422] = 0.0013f / 100, - [TestImages.Jpeg.Baseline.Testorig420] = 0.38f / 100, - [TestImages.Jpeg.Baseline.Jpeg420Small] = 0.287f / 100, - [TestImages.Jpeg.Baseline.Turtle420] = 1.0f / 100, + [TestImages.Jpeg.Baseline.Jpeg422] = 0.0013f / 100, + [TestImages.Jpeg.Baseline.Testorig420] = 0.38f / 100, + [TestImages.Jpeg.Baseline.Jpeg420Small] = 0.287f / 100, + [TestImages.Jpeg.Baseline.Turtle420] = 1.0f / 100, - // Progressive: - [TestImages.Jpeg.Issues.MissingFF00ProgressiveGirl159] = 0.34f / 100, - [TestImages.Jpeg.Issues.BadCoeffsProgressive178] = 0.38f / 100, - [TestImages.Jpeg.Progressive.Bad.BadEOF] = 0.3f / 100, - [TestImages.Jpeg.Progressive.Festzug] = 0.02f / 100, - [TestImages.Jpeg.Progressive.Fb] = 0.16f / 100, - [TestImages.Jpeg.Progressive.Progress] = 0.31f / 100, - [TestImages.Jpeg.Issues.BadZigZagProgressive385] = 0.23f / 100, - [TestImages.Jpeg.Progressive.Bad.ExifUndefType] = 0.011f / 100, - }; + // Progressive: + [TestImages.Jpeg.Issues.MissingFF00ProgressiveGirl159] = 0.34f / 100, + [TestImages.Jpeg.Issues.BadCoeffsProgressive178] = 0.38f / 100, + [TestImages.Jpeg.Progressive.Bad.BadEOF] = 0.3f / 100, + [TestImages.Jpeg.Progressive.Festzug] = 0.02f / 100, + [TestImages.Jpeg.Progressive.Fb] = 0.16f / 100, + [TestImages.Jpeg.Progressive.Progress] = 0.31f / 100, + [TestImages.Jpeg.Issues.BadZigZagProgressive385] = 0.23f / 100, + [TestImages.Jpeg.Progressive.Bad.ExifUndefType] = 0.011f / 100, + }; } } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs index 7b3e20aa2a..840cc9f685 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs @@ -4,6 +4,7 @@ using System; using System.IO; using System.Runtime.CompilerServices; +using System.Text; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Metadata; @@ -22,7 +23,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg // TODO: A JPEGsnoop & metadata expert should review if the Exif/Icc expectations are correct. // I'm seeing several entries with Exif-related names in images where we do not decode an exif profile. (- Anton) public static readonly TheoryData MetadataTestData = - new TheoryData + new() { { false, TestImages.Jpeg.Progressive.Progress, 24, false, false }, { false, TestImages.Jpeg.Progressive.Fb, 24, false, true }, @@ -42,15 +43,16 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg }; public static readonly TheoryData RatioFiles = - new TheoryData + new() { { TestImages.Jpeg.Baseline.Ratio1x1, 1, 1, PixelResolutionUnit.AspectRatio }, { TestImages.Jpeg.Baseline.Snake, 300, 300, PixelResolutionUnit.PixelsPerInch }, - { TestImages.Jpeg.Baseline.GammaDalaiLamaGray, 72, 72, PixelResolutionUnit.PixelsPerInch } + { TestImages.Jpeg.Baseline.GammaDalaiLamaGray, 72, 72, PixelResolutionUnit.PixelsPerInch }, + { TestImages.Jpeg.Issues.MultipleApp01932, 400, 400, PixelResolutionUnit.PixelsPerInch } }; public static readonly TheoryData QualityFiles = - new TheoryData + new() { { TestImages.Jpeg.Baseline.Calliphora, 80 }, { TestImages.Jpeg.Progressive.Fb, 75 }, @@ -287,5 +289,84 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg Assert.Equal(72, imageInfo.Metadata.HorizontalResolution); Assert.Equal(72, imageInfo.Metadata.VerticalResolution); }); + + [Theory] + [WithFile(TestImages.Jpeg.Issues.InvalidIptcTag, PixelTypes.Rgba32)] + public void Decode_WithInvalidIptcTag_DoesNotThrowException(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Exception ex = Record.Exception(() => + { + using Image image = provider.GetImage(JpegDecoder); + }); + 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.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs index 08d8d90382..7a24469597 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs @@ -190,9 +190,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg } [Theory] - [WithFile(TestImages.Jpeg.Baseline.ArithmeticCoding, PixelTypes.Rgba32)] - [WithFile(TestImages.Jpeg.Baseline.ArithmeticCodingProgressive, PixelTypes.Rgba32)] - [WithFile(TestImages.Jpeg.Baseline.Lossless, PixelTypes.Rgba32)] + [WithFileCollection(nameof(UnsupportedTestJpegs), PixelTypes.Rgba32)] public void ThrowsNotSupported_WithUnsupportedJpegs(TestImageProvider provider) where TPixel : unmanaged, IPixel { diff --git a/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs index 35113f14ff..3833b419c4 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); @@ -79,7 +79,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); diff --git a/tests/ImageSharp.Tests/Formats/Jpg/SpectralToPixelConversionTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/SpectralToPixelConversionTests.cs index 0071c623c6..27240831c3 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/SpectralToPixelConversionTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/SpectralToPixelConversionTests.cs @@ -45,7 +45,7 @@ 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); diff --git a/tests/ImageSharp.Tests/Formats/Jpg/Utils/LibJpegTools.cs b/tests/ImageSharp.Tests/Formats/Jpg/Utils/LibJpegTools.cs index b74e4445e5..1d54245d3a 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/Png/PngMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs index b4307af5d1..8db1d1aaf2 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -289,5 +290,34 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png Assert.Contains(meta.TextData, m => m.Keyword is "NoLang" && m.Value is "this text chunk is missing a language tag"); Assert.Contains(meta.TextData, m => m.Keyword is "NoTranslatedKeyword" && m.Value is "dieser chunk hat kein übersetztes Schlüßelwort"); } + + [Theory] + [InlineData(TestImages.Png.Issue1875)] + public void Identify_ReadsLegacyExifData(string imagePath) + { + var testFile = TestFile.Create(imagePath); + using (var stream = new MemoryStream(testFile.Bytes, false)) + { + IImageInfo imageInfo = Image.Identify(stream); + Assert.NotNull(imageInfo); + Assert.NotNull(imageInfo.Metadata.ExifProfile); + + PngMetadata meta = imageInfo.Metadata.GetFormatMetadata(PngFormat.Instance); + Assert.DoesNotContain(meta.TextData, t => t.Keyword.Equals("Raw profile type exif", StringComparison.OrdinalIgnoreCase)); + + ExifProfile exif = imageInfo.Metadata.ExifProfile; + Assert.Equal(0, exif.InvalidTags.Count); + Assert.Equal(3, exif.Values.Count); + + Assert.Equal( + "A colorful tiling of blue, red, yellow, and green 4x4 pixel blocks.", + exif.GetValue(ExifTag.ImageDescription).Value); + Assert.Equal( + "Duplicated from basn3p02.png, then image metadata modified with exiv2", + exif.GetValue(ExifTag.ImageHistory).Value); + + Assert.Equal(42, (int)exif.GetValue(ExifTag.ImageNumber).Value); + } + } } } diff --git a/tests/ImageSharp.Tests/Formats/Tiff/BigTiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/BigTiffDecoderTests.cs new file mode 100644 index 0000000000..7a02c91b8b --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tiff/BigTiffDecoderTests.cs @@ -0,0 +1,130 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +// ReSharper disable InconsistentNaming +using System; +using System.IO; +using System.Linq; +using SixLabors.ImageSharp.Formats.Tiff; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; +using Xunit; +using static SixLabors.ImageSharp.Tests.TestImages.BigTiff; + +namespace SixLabors.ImageSharp.Tests.Formats.Tiff +{ + [Collection("RunSerial")] + [Trait("Format", "Tiff")] + public class BigTiffDecoderTests : TiffDecoderBaseTester + { + [Theory] + [WithFile(BigTIFF, PixelTypes.Rgba32)] + [WithFile(BigTIFFLong, PixelTypes.Rgba32)] + [WithFile(BigTIFFLong8, PixelTypes.Rgba32)] + [WithFile(BigTIFFMotorola, PixelTypes.Rgba32)] + [WithFile(BigTIFFMotorolaLongStrips, PixelTypes.Rgba32)] + [WithFile(BigTIFFSubIFD4, PixelTypes.Rgba32)] + [WithFile(BigTIFFSubIFD8, PixelTypes.Rgba32)] + [WithFile(Indexed4_Deflate, PixelTypes.Rgba32)] + [WithFile(Indexed8_LZW, PixelTypes.Rgba32)] + [WithFile(MinIsBlack, PixelTypes.Rgba32)] + [WithFile(MinIsWhite, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + + [Theory] + [WithFile(BigTIFFLong8Tiles, PixelTypes.Rgba32)] + public void ThrowsNotSupported(TestImageProvider provider) + where TPixel : unmanaged, IPixel => Assert.Throws(() => provider.GetImage(TiffDecoder)); + + [Theory] + [WithFile(Damaged_MinIsWhite_RLE, PixelTypes.Rgba32)] + [WithFile(Damaged_MinIsBlack_RLE, PixelTypes.Rgba32)] + public void DamagedFiles(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Assert.Throws(() => TestTiffDecoder(provider)); + + using Image image = provider.GetImage(TiffDecoder); + ExifProfile exif = image.Frames.RootFrame.Metadata.ExifProfile; + + // PhotometricInterpretation is required tag: https://www.awaresystems.be/imaging/tiff/tifftags/photometricinterpretation.html + Assert.Null(exif.GetValueInternal(ExifTag.PhotometricInterpretation)); + } + + [Theory] + [InlineData(BigTIFF, 24, 64, 64, 96, 96, PixelResolutionUnit.PixelsPerInch)] + [InlineData(BigTIFFLong, 24, 64, 64, 96, 96, PixelResolutionUnit.PixelsPerInch)] + [InlineData(BigTIFFLong8, 24, 64, 64, 96, 96, PixelResolutionUnit.PixelsPerInch)] + [InlineData(BigTIFFMotorola, 24, 64, 64, 96, 96, PixelResolutionUnit.PixelsPerInch)] + [InlineData(BigTIFFMotorolaLongStrips, 24, 64, 64, 96, 96, PixelResolutionUnit.PixelsPerInch)] + [InlineData(BigTIFFSubIFD4, 24, 64, 64, 96, 96, PixelResolutionUnit.PixelsPerInch)] + [InlineData(BigTIFFSubIFD8, 24, 64, 64, 96, 96, PixelResolutionUnit.PixelsPerInch)] + [InlineData(Indexed4_Deflate, 4, 64, 64, 96, 96, PixelResolutionUnit.PixelsPerInch)] + [InlineData(Indexed8_LZW, 8, 64, 64, 96, 96, PixelResolutionUnit.PixelsPerInch)] + [InlineData(MinIsWhite, 1, 32, 32, 96, 96, PixelResolutionUnit.PixelsPerInch)] + [InlineData(MinIsBlack, 1, 32, 32, 96, 96, PixelResolutionUnit.PixelsPerInch)] + public void Identify(string imagePath, int expectedPixelSize, int expectedWidth, int expectedHeight, double expectedHResolution, double expectedVResolution, PixelResolutionUnit expectedResolutionUnit) + { + var testFile = TestFile.Create(imagePath); + using (var stream = new MemoryStream(testFile.Bytes, false)) + { + IImageInfo info = Image.Identify(stream); + + Assert.Equal(expectedPixelSize, info.PixelType?.BitsPerPixel); + Assert.Equal(expectedWidth, info.Width); + Assert.Equal(expectedHeight, info.Height); + Assert.NotNull(info.Metadata); + Assert.Equal(expectedHResolution, info.Metadata.HorizontalResolution); + Assert.Equal(expectedVResolution, info.Metadata.VerticalResolution); + Assert.Equal(expectedResolutionUnit, info.Metadata.ResolutionUnits); + + TiffMetadata tiffmeta = info.Metadata.GetTiffMetadata(); + Assert.NotNull(tiffmeta); + Assert.Equal(TiffFormatType.BigTIFF, tiffmeta.FormatType); + } + } + + [Theory] + [InlineData(BigTIFFLong, ImageSharp.ByteOrder.LittleEndian)] + [InlineData(BigTIFFMotorola, ImageSharp.ByteOrder.BigEndian)] + public void ByteOrder(string imagePath, ByteOrder expectedByteOrder) + { + var testFile = TestFile.Create(imagePath); + using (var stream = new MemoryStream(testFile.Bytes, false)) + { + IImageInfo info = Image.Identify(stream); + + Assert.NotNull(info.Metadata); + Assert.Equal(expectedByteOrder, info.Metadata.GetTiffMetadata().ByteOrder); + + stream.Seek(0, SeekOrigin.Begin); + + using var img = Image.Load(stream); + Assert.Equal(expectedByteOrder, img.Metadata.GetTiffMetadata().ByteOrder); + } + } + + [Theory] + [WithFile(BigTIFFSubIFD8, PixelTypes.Rgba32)] + public void TiffDecoder_SubIfd8(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(TiffDecoder); + + ExifProfile meta = image.Frames.RootFrame.Metadata.ExifProfile; + + Assert.Equal(0, meta.InvalidTags.Count); + Assert.Equal(6, meta.Values.Count); + Assert.Equal(64, (int)meta.GetValue(ExifTag.ImageWidth).Value); + Assert.Equal(64, (int)meta.GetValue(ExifTag.ImageLength).Value); + Assert.Equal(64, (int)meta.GetValue(ExifTag.RowsPerStrip).Value); + + Assert.Equal(1, meta.Values.Count(v => (ushort)v.Tag == (ushort)ExifTagValue.ImageWidth)); + Assert.Equal(1, meta.Values.Count(v => (ushort)v.Tag == (ushort)ExifTagValue.StripOffsets)); + Assert.Equal(1, meta.Values.Count(v => (ushort)v.Tag == (ushort)ExifTagValue.StripByteCounts)); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Tiff/BigTiffMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/BigTiffMetadataTests.cs new file mode 100644 index 0000000000..9f5b78cc3b --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tiff/BigTiffMetadataTests.cs @@ -0,0 +1,260 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.IO; +using SixLabors.ImageSharp.Formats.Tiff; +using SixLabors.ImageSharp.Formats.Tiff.Writers; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.PixelFormats; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Tiff +{ + [Trait("Format", "Tiff")] + public class BigTiffMetadataTests + { + [Fact] + public void ExifLong8() + { + var long8 = new ExifLong8(ExifTagValue.StripByteCounts); + + Assert.True(long8.TrySetValue(0)); + Assert.Equal(0UL, long8.GetValue()); + + Assert.True(long8.TrySetValue(100u)); + Assert.Equal(100UL, long8.GetValue()); + + Assert.True(long8.TrySetValue(ulong.MaxValue)); + Assert.Equal(ulong.MaxValue, long8.GetValue()); + + Assert.False(long8.TrySetValue(-65)); + Assert.Equal(ulong.MaxValue, long8.GetValue()); + } + + [Fact] + public void ExifSignedLong8() + { + var long8 = new ExifSignedLong8(ExifTagValue.ImageID); + + Assert.False(long8.TrySetValue(0)); + + Assert.True(long8.TrySetValue(0L)); + Assert.Equal(0L, long8.GetValue()); + + Assert.True(long8.TrySetValue(-100L)); + Assert.Equal(-100L, long8.GetValue()); + Assert.Equal(ExifDataType.SignedLong8, long8.DataType); + + Assert.True(long8.TrySetValue(long.MaxValue)); + Assert.Equal(long.MaxValue, long8.GetValue()); + Assert.Equal(ExifDataType.SignedLong8, long8.DataType); + } + + [Fact] + public void ExifLong8Array() + { + var long8 = new ExifLong8Array(ExifTagValue.StripOffsets); + + Assert.True(long8.TrySetValue((short)-123)); + Assert.Equal(new[] { 0UL }, long8.GetValue()); + + Assert.True(long8.TrySetValue((ushort)123)); + Assert.Equal(new[] { 123UL }, long8.GetValue()); + + Assert.True(long8.TrySetValue((short)123)); + Assert.Equal(new[] { 123UL }, long8.GetValue()); + + Assert.True(long8.TrySetValue(123)); + Assert.Equal(new[] { 123UL }, long8.GetValue()); + + Assert.True(long8.TrySetValue(123u)); + Assert.Equal(new[] { 123UL }, long8.GetValue()); + + Assert.True(long8.TrySetValue(123L)); + Assert.Equal(new[] { 123UL }, long8.GetValue()); + + Assert.True(long8.TrySetValue(123UL)); + Assert.Equal(new[] { 123UL }, long8.GetValue()); + + Assert.True(long8.TrySetValue(new short[] { -1, 2, -3, 4 })); + Assert.Equal(new ulong[] { 0, 2UL, 0, 4UL }, long8.GetValue()); + + Assert.True(long8.TrySetValue(new[] { 1, 2, 3, 4 })); + Assert.Equal(new[] { 1UL, 2UL, 3UL, 4UL }, long8.GetValue()); + Assert.Equal(ExifDataType.Long, long8.DataType); + + Assert.True(long8.TrySetValue(new[] { 1, 2, 3, 4, long.MaxValue })); + Assert.Equal(new[] { 1UL, 2UL, 3UL, 4UL, (ulong)long.MaxValue }, long8.GetValue()); + Assert.Equal(ExifDataType.Long8, long8.DataType); + } + + [Fact] + public void ExifSignedLong8Array() + { + var long8 = new ExifSignedLong8Array(ExifTagValue.StripOffsets); + + Assert.True(long8.TrySetValue(new[] { 0L })); + Assert.Equal(new[] { 0L }, long8.GetValue()); + Assert.Equal(ExifDataType.SignedLong8, long8.DataType); + + Assert.True(long8.TrySetValue(new[] { -1L, 2L, long.MinValue, 4L })); + Assert.Equal(new[] { -1L, 2L, long.MinValue, 4L }, long8.GetValue()); + Assert.Equal(ExifDataType.SignedLong8, long8.DataType); + } + + [Fact] + public void NotCoveredTags() + { + using var input = new Image(10, 10); + + var testTags = new Dictionary + { + { new ExifTag((ExifTagValue)0xdd01), (ExifDataType.SingleFloat, new float[] { 1.2f, 2.3f, 4.5f }) }, + { new ExifTag((ExifTagValue)0xdd02), (ExifDataType.SingleFloat, 2.345f) }, + { new ExifTag((ExifTagValue)0xdd03), (ExifDataType.DoubleFloat, new double[] { 4.5, 6.7 }) }, + { new ExifTag((ExifTagValue)0xdd04), (ExifDataType.DoubleFloat, 8.903) }, + { new ExifTag((ExifTagValue)0xdd05), (ExifDataType.SignedByte, (sbyte)-3) }, + { new ExifTag((ExifTagValue)0xdd06), (ExifDataType.SignedByte, new sbyte[] { -3, 0, 5 }) }, + { new ExifTag((ExifTagValue)0xdd07), (ExifDataType.SignedLong, new int[] { int.MinValue, 1, int.MaxValue }) }, + { new ExifTag((ExifTagValue)0xdd08), (ExifDataType.Long, new uint[] { 0, 1, uint.MaxValue }) }, + { new ExifTag((ExifTagValue)0xdd09), (ExifDataType.SignedShort, (short)-1234) }, + { new ExifTag((ExifTagValue)0xdd10), (ExifDataType.Short, (ushort)1234) }, + }; + + // arrange + var values = new List(); + foreach (KeyValuePair tag in testTags) + { + ExifValue newExifValue = ExifValues.Create((ExifTagValue)(ushort)tag.Key, tag.Value.DataType, tag.Value.Value is Array); + + Assert.True(newExifValue.TrySetValue(tag.Value.Value)); + values.Add(newExifValue); + } + + input.Frames.RootFrame.Metadata.ExifProfile = new ExifProfile(values, Array.Empty()); + + // act + var encoder = new TiffEncoder(); + using var memStream = new MemoryStream(); + input.Save(memStream, encoder); + + // assert + memStream.Position = 0; + using var output = Image.Load(memStream); + ImageFrameMetadata loadedFrameMetadata = output.Frames.RootFrame.Metadata; + foreach (KeyValuePair tag in testTags) + { + IExifValue exifValue = loadedFrameMetadata.ExifProfile.GetValueInternal(tag.Key); + Assert.NotNull(exifValue); + object value = exifValue.GetValue(); + + Assert.Equal(tag.Value.DataType, exifValue.DataType); + { + Assert.Equal(value, tag.Value.Value); + } + } + } + + [Fact] + public void NotCoveredTags64bit() + { + var testTags = new Dictionary + { + { new ExifTag((ExifTagValue)0xdd11), (ExifDataType.Long8, ulong.MaxValue) }, + { new ExifTag((ExifTagValue)0xdd12), (ExifDataType.SignedLong8, long.MaxValue) }, + //// WriteIfdTags64Bit: arrays aren't support (by our code) + ////{ new ExifTag((ExifTagValue)0xdd13), (ExifDataType.Long8, new ulong[] { 0, 1234, 56789UL, ulong.MaxValue }) }, + ////{ new ExifTag((ExifTagValue)0xdd14), (ExifDataType.SignedLong8, new long[] { -1234, 56789L, long.MaxValue }) }, + }; + + var values = new List(); + foreach (KeyValuePair tag in testTags) + { + ExifValue newExifValue = ExifValues.Create((ExifTagValue)(ushort)tag.Key, tag.Value.DataType, tag.Value.Value is Array); + + Assert.True(newExifValue.TrySetValue(tag.Value.Value)); + values.Add(newExifValue); + } + + // act + byte[] inputBytes = WriteIfdTags64Bit(values); + Configuration config = Configuration.Default; + var reader = new EntryReader( + new MemoryStream(inputBytes), + BitConverter.IsLittleEndian ? ByteOrder.LittleEndian : ByteOrder.BigEndian, + config.MemoryAllocator); + + reader.ReadTags(true, 0); + + List outputTags = reader.Values; + + // assert + foreach (KeyValuePair tag in testTags) + { + IExifValue exifValue = outputTags.Find(t => t.Tag == tag.Key); + Assert.NotNull(exifValue); + object value = exifValue.GetValue(); + + Assert.Equal(tag.Value.DataType, exifValue.DataType); + { + Assert.Equal(value, tag.Value.Value); + } + } + } + + private static byte[] WriteIfdTags64Bit(List values) + { + byte[] buffer = new byte[8]; + var ms = new MemoryStream(); + var writer = new TiffStreamWriter(ms); + WriteLong8(writer, buffer, (ulong)values.Count); + + foreach (IExifValue entry in values) + { + writer.Write((ushort)entry.Tag); + writer.Write((ushort)entry.DataType); + WriteLong8(writer, buffer, ExifWriter.GetNumberOfComponents(entry)); + + uint length = ExifWriter.GetLength(entry); + + Assert.True(length <= 8); + + if (length <= 8) + { + int sz = ExifWriter.WriteValue(entry, buffer, 0); + DebugGuard.IsTrue(sz == length, "Incorrect number of bytes written"); + + // write padded + writer.BaseStream.Write(buffer.AsSpan(0, sz)); + int d = sz % 8; + if (d != 0) + { + writer.BaseStream.Write(new byte[d]); + } + } + } + + WriteLong8(writer, buffer, 0); + + return ms.ToArray(); + } + + private static void WriteLong8(TiffStreamWriter writer, byte[] buffer, ulong value) + { + if (writer.IsLittleEndian) + { + BinaryPrimitives.WriteUInt64LittleEndian(buffer, value); + } + else + { + BinaryPrimitives.WriteUInt64BigEndian(buffer, value); + } + + writer.BaseStream.Write(buffer); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderBaseTester.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderBaseTester.cs new file mode 100644 index 0000000000..aeadae2557 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderBaseTester.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +// ReSharper disable InconsistentNaming +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Tiff; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; +using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; + +namespace SixLabors.ImageSharp.Tests.Formats.Tiff +{ + public abstract class TiffDecoderBaseTester + { + protected static TiffDecoder TiffDecoder => new TiffDecoder(); + + protected static MagickReferenceDecoder ReferenceDecoder => new MagickReferenceDecoder(); + + protected static void TestTiffDecoder(TestImageProvider provider, IImageDecoder referenceDecoder = null, bool useExactComparer = true, float compareTolerance = 0.001f) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(TiffDecoder); + image.DebugSave(provider); + image.CompareToOriginal( + provider, + useExactComparer ? ImageComparer.Exact : ImageComparer.Tolerant(compareTolerance), + referenceDecoder ?? ReferenceDecoder); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index f8256ead9f..ea0544acf4 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -4,32 +4,25 @@ // ReSharper disable InconsistentNaming using System; using System.IO; -using SixLabors.ImageSharp.Formats; -using SixLabors.ImageSharp.Formats.Tiff; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; -using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; using Xunit; using static SixLabors.ImageSharp.Tests.TestImages.Tiff; namespace SixLabors.ImageSharp.Tests.Formats.Tiff { [Trait("Format", "Tiff")] - public class TiffDecoderTests + public class TiffDecoderTests : TiffDecoderBaseTester { public static readonly string[] MultiframeTestImages = Multiframes; - private static TiffDecoder TiffDecoder => new(); - - private static MagickReferenceDecoder ReferenceDecoder => new(); - [Theory] [WithFile(RgbUncompressedTiled, PixelTypes.Rgba32)] [WithFile(MultiframeDifferentSize, PixelTypes.Rgba32)] [WithFile(MultiframeDifferentVariants, PixelTypes.Rgba32)] public void ThrowsNotSupported(TestImageProvider provider) - where TPixel : unmanaged, IPixel => Assert.Throws(() => provider.GetImage(TiffDecoder)); + where TPixel : unmanaged, IPixel => Assert.Throws(() => provider.GetImage(TiffDecoder)); [Theory] [InlineData(RgbUncompressed, 24, 256, 256, 300, 300, PixelResolutionUnit.PixelsPerInch)] @@ -406,16 +399,5 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff image.DebugSaveMultiFrame(provider); image.CompareToOriginalMultiFrame(provider, ImageComparer.Exact, ReferenceDecoder); } - - private static void TestTiffDecoder(TestImageProvider provider, IImageDecoder referenceDecoder = null, bool useExactComparer = true, float compareTolerance = 0.001f) - where TPixel : unmanaged, IPixel - { - using Image image = provider.GetImage(TiffDecoder); - image.DebugSave(provider); - image.CompareToOriginal( - provider, - useExactComparer ? ImageComparer.Exact : ImageComparer.Tolerant(compareTolerance), - referenceDecoder ?? ReferenceDecoder); - } } } diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs index aded52cd9b..93ca611c9e 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/Tiff/TiffMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs index 7715ac3a38..6a47a95771 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs @@ -10,6 +10,7 @@ using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Iptc; +using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; using Xunit; @@ -132,7 +133,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff { Assert.NotNull(rootFrameMetaData.XmpProfile); Assert.NotNull(rootFrameMetaData.ExifProfile); - Assert.Equal(2599, rootFrameMetaData.XmpProfile.Length); + Assert.Equal(2599, rootFrameMetaData.XmpProfile.Data.Length); Assert.Equal(26, rootFrameMetaData.ExifProfile.Values.Count); } } @@ -163,7 +164,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff Assert.Equal(32, rootFrame.Width); Assert.Equal(32, rootFrame.Height); Assert.NotNull(rootFrame.Metadata.XmpProfile); - Assert.Equal(2599, rootFrame.Metadata.XmpProfile.Length); + Assert.Equal(2599, rootFrame.Metadata.XmpProfile.Data.Length); ExifProfile exifProfile = rootFrame.Metadata.ExifProfile; TiffFrameMetadata tiffFrameMetadata = rootFrame.Metadata.GetTiffMetadata(); @@ -251,7 +252,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff ImageMetadata inputMetaData = image.Metadata; ImageFrame rootFrameInput = image.Frames.RootFrame; TiffFrameMetadata frameMetaInput = rootFrameInput.Metadata.GetTiffMetadata(); - byte[] xmpProfileInput = rootFrameInput.Metadata.XmpProfile; + XmpProfile xmpProfileInput = rootFrameInput.Metadata.XmpProfile; ExifProfile exifProfileInput = rootFrameInput.Metadata.ExifProfile; Assert.Equal(TiffCompression.Lzw, frameMetaInput.Compression); @@ -270,7 +271,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff ImageFrame rootFrameEncodedImage = encodedImage.Frames.RootFrame; TiffFrameMetadata tiffMetaDataEncodedRootFrame = rootFrameEncodedImage.Metadata.GetTiffMetadata(); ExifProfile encodedImageExifProfile = rootFrameEncodedImage.Metadata.ExifProfile; - byte[] encodedImageXmpProfile = rootFrameEncodedImage.Metadata.XmpProfile; + XmpProfile encodedImageXmpProfile = rootFrameEncodedImage.Metadata.XmpProfile; Assert.Equal(TiffBitsPerPixel.Bit4, tiffMetaDataEncodedRootFrame.BitsPerPixel); Assert.Equal(TiffCompression.Lzw, tiffMetaDataEncodedRootFrame.Compression); @@ -288,7 +289,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff Assert.Equal(exifProfileInput.GetValue(ExifTag.XResolution).Value.ToDouble(), encodedImageExifProfile.GetValue(ExifTag.XResolution).Value.ToDouble()); Assert.Equal(exifProfileInput.GetValue(ExifTag.YResolution).Value.ToDouble(), encodedImageExifProfile.GetValue(ExifTag.YResolution).Value.ToDouble()); - Assert.Equal(xmpProfileInput, encodedImageXmpProfile); + Assert.NotNull(xmpProfileInput); + Assert.NotNull(encodedImageXmpProfile); + Assert.Equal(xmpProfileInput.Data, encodedImageXmpProfile.Data); Assert.Equal("IrfanView", exifProfileInput.GetValue(ExifTag.Software).Value); Assert.Equal("This is Название", exifProfileInput.GetValue(ExifTag.ImageDescription).Value); diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs index 22342e612f..1c92fdf335 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs @@ -19,6 +19,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Webp private static MagickReferenceDecoder ReferenceDecoder => new(); + private static string TestImageLossyHorizontalFilterPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, Lossy.AlphaCompressedHorizontalFilter); + + private static string TestImageLossyVerticalFilterPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, Lossy.AlphaCompressedVerticalFilter); + private static string TestImageLossySimpleFilterPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, Lossy.SimpleFilter02); private static string TestImageLossyComplexFilterPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, Lossy.BikeComplexFilter); @@ -365,6 +369,26 @@ namespace SixLabors.ImageSharp.Tests.Formats.Webp }); #if SUPPORTS_RUNTIME_INTRINSICS + private static void RunDecodeLossyWithHorizontalFilter() + { + var provider = TestImageProvider.File(TestImageLossyHorizontalFilterPath); + using (Image image = provider.GetImage(WebpDecoder)) + { + image.DebugSave(provider); + image.CompareToOriginal(provider, ReferenceDecoder); + } + } + + private static void RunDecodeLossyWithVerticalFilter() + { + var provider = TestImageProvider.File(TestImageLossyVerticalFilterPath); + using (Image image = provider.GetImage(WebpDecoder)) + { + image.DebugSave(provider); + image.CompareToOriginal(provider, ReferenceDecoder); + } + } + private static void RunDecodeLossyWithSimpleFilterTest() { var provider = TestImageProvider.File(TestImageLossySimpleFilterPath); @@ -385,6 +409,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Webp } } + [Fact] + public void DecodeLossyWithHorizontalFilter_WithoutHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunDecodeLossyWithHorizontalFilter, HwIntrinsics.DisableHWIntrinsic); + + [Fact] + public void DecodeLossyWithVerticalFilter_WithoutHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunDecodeLossyWithVerticalFilter, HwIntrinsics.DisableHWIntrinsic); + [Fact] public void DecodeLossyWithSimpleFilterTest_WithoutHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunDecodeLossyWithSimpleFilterTest, HwIntrinsics.DisableHWIntrinsic); diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs index cad39224e5..7c74429edc 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs @@ -18,7 +18,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Webp private static string TestImageLossyFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, Lossy.NoFilter06); [Theory] - [WithFile(Flag, PixelTypes.Rgba32, WebpFileFormatType.Lossless)] // if its not a webp input image, it should default to lossless. + [WithFile(Flag, PixelTypes.Rgba32, WebpFileFormatType.Lossy)] // If its not a webp input image, it should default to lossy. [WithFile(Lossless.NoTransform1, PixelTypes.Rgba32, WebpFileFormatType.Lossless)] [WithFile(Lossy.Bike, PixelTypes.Rgba32, WebpFileFormatType.Lossy)] public void Encode_PreserveRatio(TestImageProvider provider, WebpFileFormatType expectedFormat) @@ -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 a051de1c01..7fba86b4fe 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs @@ -63,6 +63,26 @@ 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) + where TPixel : unmanaged, IPixel + { + var decoder = new WebpDecoder { IgnoreMetadata = ignoreMetadata }; + + using Image image = await provider.GetImageAsync(decoder); + if (ignoreMetadata) + { + Assert.Null(image.Metadata.XmpProfile); + } + else + { + Assert.NotNull(image.Metadata.XmpProfile); + Assert.NotEmpty(image.Metadata.XmpProfile.Data); + } + } + [Theory] [InlineData(WebpFileFormatType.Lossy)] [InlineData(WebpFileFormatType.Lossless)] diff --git a/tests/ImageSharp.Tests/IO/BufferedReadStreamTests.cs b/tests/ImageSharp.Tests/IO/BufferedReadStreamTests.cs index 8e73218647..f968b16f00 100644 --- a/tests/ImageSharp.Tests/IO/BufferedReadStreamTests.cs +++ b/tests/ImageSharp.Tests/IO/BufferedReadStreamTests.cs @@ -314,7 +314,7 @@ namespace SixLabors.ImageSharp.Tests.IO [Theory] [MemberData(nameof(BufferSizes))] - public void BufferedStreamThrowsOnBadPosition(int bufferSize) + public void BufferedStreamThrowsOnNegativePosition(int bufferSize) { this.configuration.StreamProcessingBufferSize = bufferSize; using (MemoryStream stream = this.CreateTestStream(bufferSize)) @@ -322,15 +322,14 @@ namespace SixLabors.ImageSharp.Tests.IO using (var reader = new BufferedReadStream(this.configuration, stream)) { Assert.Throws(() => reader.Position = -stream.Length); - Assert.Throws(() => reader.Position = stream.Length + 1); } } } - [Fact] - public void BufferedStreamCanSetPositionToEnd() + [Theory] + [MemberData(nameof(BufferSizes))] + public void BufferedStreamCanSetPositionToEnd(int bufferSize) { - var bufferSize = 8; this.configuration.StreamProcessingBufferSize = bufferSize; using (MemoryStream stream = this.CreateTestStream(bufferSize * 2)) { @@ -341,6 +340,21 @@ namespace SixLabors.ImageSharp.Tests.IO } } + [Theory] + [MemberData(nameof(BufferSizes))] + public void BufferedStreamCanSetPositionPastTheEnd(int bufferSize) + { + this.configuration.StreamProcessingBufferSize = bufferSize; + using (MemoryStream stream = this.CreateTestStream(bufferSize * 2)) + { + using (var reader = new BufferedReadStream(this.configuration, stream)) + { + reader.Position = reader.Length + 1; + Assert.Equal(stream.Length + 1, stream.Position); + } + } + } + private MemoryStream CreateTestStream(int length) { var buffer = new byte[length]; diff --git a/tests/ImageSharp.Tests/Image/ImageTests.Identify.cs b/tests/ImageSharp.Tests/Image/ImageTests.Identify.cs index 271aa30cf4..93f73c3d3f 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.Identify.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.Identify.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System.IO; +using System.IO.Compression; using System.Threading.Tasks; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Tests.TestUtilities; @@ -138,6 +139,32 @@ namespace SixLabors.ImageSharp.Tests Assert.Null(type); } + [Fact] + public void FromStream_ZeroLength_ReturnsNull() + { + // https://github.com/SixLabors/ImageSharp/issues/1903 + using var zipFile = new ZipArchive(new MemoryStream( + new byte[] + { + 0x50, 0x4B, 0x03, 0x04, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x77, 0xAF, + 0x94, 0x53, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x6D, 0x79, 0x73, 0x74, 0x65, 0x72, + 0x79, 0x50, 0x4B, 0x01, 0x02, 0x3F, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x77, 0xAF, 0x94, 0x53, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x24, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6D, + 0x79, 0x73, 0x74, 0x65, 0x72, 0x79, 0x0A, 0x00, 0x20, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x18, 0x00, 0x46, 0x82, 0xFF, 0x91, 0x27, 0xF6, + 0xD7, 0x01, 0x55, 0xA1, 0xF9, 0x91, 0x27, 0xF6, 0xD7, 0x01, 0x55, 0xA1, + 0xF9, 0x91, 0x27, 0xF6, 0xD7, 0x01, 0x50, 0x4B, 0x05, 0x06, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x59, 0x00, 0x00, 0x00, 0x25, 0x00, + 0x00, 0x00, 0x00, 0x00 + })); + using Stream stream = zipFile.Entries[0].Open(); + IImageInfo info = Image.Identify(stream); + Assert.Null(info); + } + [Fact] public async Task FromStreamAsync_GlobalConfiguration_NoFormat() { @@ -188,6 +215,32 @@ namespace SixLabors.ImageSharp.Tests Assert.Equal(ExpectedGlobalFormat, res.Format); } + [Fact] + public async Task FromStreamAsync_ZeroLength_ReturnsNull() + { + // https://github.com/SixLabors/ImageSharp/issues/1903 + using var zipFile = new ZipArchive(new MemoryStream( + new byte[] + { + 0x50, 0x4B, 0x03, 0x04, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x77, 0xAF, + 0x94, 0x53, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x6D, 0x79, 0x73, 0x74, 0x65, 0x72, + 0x79, 0x50, 0x4B, 0x01, 0x02, 0x3F, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x77, 0xAF, 0x94, 0x53, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x24, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6D, + 0x79, 0x73, 0x74, 0x65, 0x72, 0x79, 0x0A, 0x00, 0x20, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x18, 0x00, 0x46, 0x82, 0xFF, 0x91, 0x27, 0xF6, + 0xD7, 0x01, 0x55, 0xA1, 0xF9, 0x91, 0x27, 0xF6, 0xD7, 0x01, 0x55, 0xA1, + 0xF9, 0x91, 0x27, 0xF6, 0xD7, 0x01, 0x50, 0x4B, 0x05, 0x06, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x59, 0x00, 0x00, 0x00, 0x25, 0x00, + 0x00, 0x00, 0x00, 0x00 + })); + using Stream stream = zipFile.Entries[0].Open(); + IImageInfo info = await Image.IdentifyAsync(stream); + Assert.Null(info); + } + [Fact] public async Task FromPathAsync_CustomConfiguration() { diff --git a/tests/ImageSharp.Tests/Image/LargeImageIntegrationTests.cs b/tests/ImageSharp.Tests/Image/LargeImageIntegrationTests.cs index b2ee9d673e..357d02be4b 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,40 @@ 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) + { + 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 471287006f..28c778787a 100644 --- a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj +++ b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj @@ -47,6 +47,7 @@ + diff --git a/tests/ImageSharp.Tests/Memory/Allocators/MemoryDiagnosticsTests.cs b/tests/ImageSharp.Tests/Memory/Allocators/MemoryDiagnosticsTests.cs new file mode 100644 index 0000000000..5fab655cb3 --- /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 7fb3b7b7bb..4b808e8630 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.cs b/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedMemoryPoolTests.cs index 00acce64eb..7d98eff611 100644 --- a/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedMemoryPoolTests.cs +++ b/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedMemoryPoolTests.cs @@ -253,35 +253,40 @@ namespace SixLabors.ImageSharp.Tests.Memory.Allocators [InlineData(true)] public void RentReturnRelease_SubsequentRentReturnsDifferentHandles(bool multiple) { - var pool = new UniformUnmanagedMemoryPool(16, 16); - using var cleanup = new CleanupUtil(pool); - UnmanagedMemoryHandle b0 = pool.Rent(); - IntPtr h0 = b0.Handle; - UnmanagedMemoryHandle b1 = pool.Rent(); - IntPtr h1 = b1.Handle; - pool.Return(b0); - pool.Return(b1); - pool.Release(); + RemoteExecutor.Invoke(RunTest, multiple.ToString()).Dispose(); - // Do some unmanaged allocations to make sure new pool buffers are different: - IntPtr[] dummy = Enumerable.Range(0, 100).Select(_ => Marshal.AllocHGlobal(16)).ToArray(); - cleanup.Register(dummy); - - if (multiple) - { - UnmanagedMemoryHandle b = pool.Rent(); - cleanup.Register(b); - Assert.NotEqual(h0, b.Handle); - Assert.NotEqual(h1, b.Handle); - } - else + static void RunTest(string multipleInner) { - UnmanagedMemoryHandle[] b = pool.Rent(2); - cleanup.Register(b); - Assert.NotEqual(h0, b[0].Handle); - Assert.NotEqual(h1, b[0].Handle); - Assert.NotEqual(h0, b[1].Handle); - Assert.NotEqual(h1, b[1].Handle); + var pool = new UniformUnmanagedMemoryPool(16, 16); + using var cleanup = new CleanupUtil(pool); + UnmanagedMemoryHandle b0 = pool.Rent(); + IntPtr h0 = b0.Handle; + UnmanagedMemoryHandle b1 = pool.Rent(); + IntPtr h1 = b1.Handle; + pool.Return(b0); + pool.Return(b1); + pool.Release(); + + // Do some unmanaged allocations to make sure new pool buffers are different: + IntPtr[] dummy = Enumerable.Range(0, 100).Select(_ => Marshal.AllocHGlobal(16)).ToArray(); + cleanup.Register(dummy); + + if (bool.Parse(multipleInner)) + { + UnmanagedMemoryHandle b = pool.Rent(); + cleanup.Register(b); + Assert.NotEqual(h0, b.Handle); + Assert.NotEqual(h1, b.Handle); + } + else + { + UnmanagedMemoryHandle[] b = pool.Rent(2); + cleanup.Register(b); + Assert.NotEqual(h0, b[0].Handle); + Assert.NotEqual(h1, b[0].Handle); + Assert.NotEqual(h0, b[1].Handle); + Assert.NotEqual(h1, b[1].Handle); + } } } diff --git a/tests/ImageSharp.Tests/Memory/BufferAreaTests.cs b/tests/ImageSharp.Tests/Memory/BufferAreaTests.cs index 76e55aa3a1..12b7c74abb 100644 --- a/tests/ImageSharp.Tests/Memory/BufferAreaTests.cs +++ b/tests/ImageSharp.Tests/Memory/BufferAreaTests.cs @@ -68,7 +68,7 @@ namespace SixLabors.ImageSharp.Tests.Memory Buffer2DRegion region = buffer.GetRegion(r); - Span span = region.GetRowSpan(y); + Span span = region.DangerousGetRowSpan(y); Assert.Equal(w, span.Length); diff --git a/tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs b/tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs index f1a90d43e7..dd8ae3d5ac 100644 --- a/tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs +++ b/tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs @@ -1,10 +1,10 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. -using System.Linq; using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Icc; +using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using Xunit; using ExifProfile = SixLabors.ImageSharp.Metadata.Profiles.Exif.ExifProfile; using ExifTag = SixLabors.ImageSharp.Metadata.Profiles.Exif.ExifTag; @@ -41,10 +41,10 @@ namespace SixLabors.ImageSharp.Tests.Metadata public void CloneIsDeep() { // arrange - byte[] xmpProfile = { 1, 2, 3 }; var exifProfile = new ExifProfile(); exifProfile.SetValue(ExifTag.Software, "UnitTest"); exifProfile.SetValue(ExifTag.Artist, "UnitTest"); + var xmpProfile = new XmpProfile(new byte[0]); var iccProfile = new IccProfile() { Header = new IccProfileHeader() @@ -72,8 +72,8 @@ namespace SixLabors.ImageSharp.Tests.Metadata Assert.NotNull(clone.IptcProfile); Assert.False(metaData.ExifProfile.Equals(clone.ExifProfile)); Assert.True(metaData.ExifProfile.Values.Count == clone.ExifProfile.Values.Count); - Assert.False(metaData.XmpProfile.Equals(clone.XmpProfile)); - Assert.True(metaData.XmpProfile.SequenceEqual(clone.XmpProfile)); + Assert.False(ReferenceEquals(metaData.XmpProfile, clone.XmpProfile)); + Assert.True(metaData.XmpProfile.Data.Equals(clone.XmpProfile.Data)); Assert.False(metaData.GetGifMetadata().Equals(clone.GetGifMetadata())); Assert.False(metaData.IccProfile.Equals(clone.IccProfile)); Assert.False(metaData.IptcProfile.Equals(clone.IptcProfile)); diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs index ebc0968524..a859852279 100644 --- a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs +++ b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Buffers.Binary; using System.Collections.Generic; using System.IO; using System.Linq; @@ -50,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] @@ -86,6 +90,18 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif new ExifProfile(Array.Empty()); } + [Fact] + public void EmptyWriter() + { + var profile = new ExifProfile() { Parts = ExifParts.GpsTags }; + profile.SetValue(ExifTag.Copyright, "Copyright text"); + + byte[] bytes = profile.ToByteArray(); + + Assert.NotNull(bytes); + Assert.Empty(bytes); + } + [Fact] public void ConstructorCopy() { @@ -185,9 +201,9 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif 2 x due to use of non-standard padding tag 0xEA1C listed in EXIF Tool. We can read those values but adhere strictly to the 2.3.1 specification when writing. (TODO: Support 2.3.2) https://exiftool.org/TagNames/EXIF.html */ - [InlineData(TestImageWriteFormat.Jpeg, 16)] - [InlineData(TestImageWriteFormat.Png, 16)] - [InlineData(TestImageWriteFormat.WebpLossless, 16)] + [InlineData(TestImageWriteFormat.Jpeg, 18)] + [InlineData(TestImageWriteFormat.Png, 18)] + [InlineData(TestImageWriteFormat.WebpLossless, 18)] public void SetValue(TestImageWriteFormat imageFormat, int expectedProfileValueCount) { Image image = TestFile.Create(TestImages.Jpeg.Baseline.Floorplan).CreateRgba32Image(); @@ -420,7 +436,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif Assert.Equal(2, profile.Values.Count(v => (ExifTagValue)(ushort)v.Tag == ExifTagValue.DateTime)); byte[] bytes = profile.ToByteArray(); - Assert.Equal(525, bytes.Length); + Assert.Equal(531, bytes.Length); var profile2 = new ExifProfile(bytes); Assert.Equal(25, profile2.Values.Count); @@ -487,6 +503,22 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif return profile; } + [Fact] + public void IfdStructure() + { + var exif = new ExifProfile(); + exif.SetValue(ExifTag.XPAuthor, "Dan Petitt"); + + Span actualBytes = exif.ToByteArray(); + + // Assert + int ifdOffset = ExifConstants.LittleEndianByteOrderMarker.Length; + Assert.Equal(8U, BinaryPrimitives.ReadUInt32LittleEndian(actualBytes.Slice(ifdOffset, 4))); + + int nextIfdPointerOffset = ExifConstants.LittleEndianByteOrderMarker.Length + 4 + 2 + 12; + Assert.Equal(0U, BinaryPrimitives.ReadUInt32LittleEndian(actualBytes.Slice(nextIfdPointerOffset, 4))); + } + internal static ExifProfile GetExifProfile() { using Image image = TestFile.Create(TestImages.Jpeg.Baseline.Floorplan).CreateRgba32Image(); @@ -557,7 +589,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif // todo: duplicate tags Assert.Equal(2, profile.Values.Count(v => (ushort)v.Tag == 59932)); - Assert.Equal(16, profile.Values.Count); + Assert.Equal(18, profile.Values.Count); foreach (IExifValue value in profile.Values) { diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/Values/ExifValuesTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/Values/ExifValuesTests.cs index 3358b1f977..aaad4e2306 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) @@ -394,6 +404,34 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif.Values Assert.Equal(expected, typed.Value); } + [Fact] + public void NumberTests() + { + Number value1 = ushort.MaxValue; + Number value2 = ushort.MaxValue; + Assert.True(value1 == value2); + + value2 = short.MaxValue; + Assert.True(value1 != value2); + + value1 = -1; + value2 = -2; + Assert.True(value1 > value2); + + value1 = -6; + Assert.True(value1 <= value2); + + value1 = 10; + value2 = 10; + Assert.True(value1 >= value2); + + Assert.True(value1.Equals(value2)); + Assert.True(value1.GetHashCode() == value2.GetHashCode()); + + value1 = 1; + Assert.False(value1.Equals(value2)); + } + [Theory] [MemberData(nameof(NumberTags))] public void ExifNumberTests(ExifTag tag) @@ -408,6 +446,9 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif.Values var typed = (ExifNumber)value; Assert.Equal(expected, typed.Value); + + typed.Value = ushort.MaxValue + 1; + Assert.True(expected < typed.Value); } [Theory] @@ -422,6 +463,15 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif.Values var typed = (ExifNumberArray)value; Assert.Equal(expected, typed.Value); + + Assert.True(value.TrySetValue(int.MaxValue)); + Assert.Equal(new[] { (Number)int.MaxValue }, value.GetValue()); + + Assert.True(value.TrySetValue(new[] { 1u, 2u, 5u })); + Assert.Equal(new[] { (Number)1u, (Number)2u, (Number)5u }, value.GetValue()); + + Assert.True(value.TrySetValue(new[] { (short)1, (short)2, (short)5 })); + Assert.Equal(new[] { (Number)(short)1, (Number)(short)2, (Number)(short)5 }, value.GetValue()); } [Theory] @@ -553,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 new file mode 100644 index 0000000000..81dad699a1 --- /dev/null +++ b/tests/ImageSharp.Tests/Metadata/Profiles/XMP/XmpProfileTests.cs @@ -0,0 +1,260 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; +using System.Text; +using System.Xml.Linq; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Formats.Tiff; +using SixLabors.ImageSharp.Formats.Webp; +using SixLabors.ImageSharp.Metadata.Profiles.Xmp; +using SixLabors.ImageSharp.PixelFormats; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Xmp +{ + public class XmpProfileTests + { + private static GifDecoder GifDecoder => new() { IgnoreMetadata = false }; + + private static JpegDecoder JpegDecoder => new() { IgnoreMetadata = false }; + + private static PngDecoder PngDecoder => new() { IgnoreMetadata = false }; + + private static TiffDecoder TiffDecoder => new() { IgnoreMetadata = false }; + + private static WebpDecoder WebpDecoder => new() { IgnoreMetadata = false }; + + [Theory] + [WithFile(TestImages.Gif.Receipt, PixelTypes.Rgba32)] + public async void ReadXmpMetadata_FromGif_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = await provider.GetImageAsync(GifDecoder)) + { + XmpProfile actual = image.Metadata.XmpProfile ?? image.Frames.RootFrame.Metadata.XmpProfile; + XmpProfileContainsExpectedValues(actual); + } + } + + [Theory] + [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) + where TPixel : unmanaged, IPixel + { + using (Image image = await provider.GetImageAsync(JpegDecoder)) + { + XmpProfile actual = image.Metadata.XmpProfile ?? image.Frames.RootFrame.Metadata.XmpProfile; + XmpProfileContainsExpectedValues(actual); + } + } + + [Theory] + [WithFile(TestImages.Png.XmpColorPalette, PixelTypes.Rgba32)] + public async void ReadXmpMetadata_FromPng_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = await provider.GetImageAsync(PngDecoder)) + { + XmpProfile actual = image.Metadata.XmpProfile ?? image.Frames.RootFrame.Metadata.XmpProfile; + XmpProfileContainsExpectedValues(actual); + } + } + + [Theory] + [WithFile(TestImages.Tiff.SampleMetadata, PixelTypes.Rgba32)] + public async void ReadXmpMetadata_FromTiff_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = await provider.GetImageAsync(TiffDecoder)) + { + XmpProfile actual = image.Metadata.XmpProfile ?? image.Frames.RootFrame.Metadata.XmpProfile; + XmpProfileContainsExpectedValues(actual); + } + } + + [Theory] + [WithFile(TestImages.Webp.Lossy.WithXmp, PixelTypes.Rgba32)] + public async void ReadXmpMetadata_FromWebp_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = await provider.GetImageAsync(WebpDecoder)) + { + XmpProfile actual = image.Metadata.XmpProfile ?? image.Frames.RootFrame.Metadata.XmpProfile; + XmpProfileContainsExpectedValues(actual); + } + } + + [Fact] + public void XmpProfile_ToFromByteArray_ReturnsClone() + { + // arrange + XmpProfile profile = CreateMinimalXmlProfile(); + byte[] original = profile.ToByteArray(); + + // act + byte[] actual = profile.ToByteArray(); + + // assert + Assert.False(ReferenceEquals(original, actual)); + } + + [Fact] + public void XmpProfile_CloneIsDeep() + { + // arrange + XmpProfile profile = CreateMinimalXmlProfile(); + byte[] original = profile.ToByteArray(); + + // act + XmpProfile clone = profile.DeepClone(); + byte[] actual = clone.ToByteArray(); + + // assert + Assert.False(ReferenceEquals(original, actual)); + } + + [Fact] + public void WritingGif_PreservesXmpProfile() + { + // arrange + var image = new Image(1, 1); + XmpProfile original = CreateMinimalXmlProfile(); + image.Metadata.XmpProfile = original; + var encoder = new GifEncoder(); + + // act + using Image reloadedImage = WriteAndRead(image, encoder); + + // assert + XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile; + XmpProfileContainsExpectedValues(actual); + Assert.Equal(original.Data, actual.Data); + } + + [Fact] + public void WritingJpeg_PreservesXmpProfile() + { + // arrange + var image = new Image(1, 1); + XmpProfile original = CreateMinimalXmlProfile(); + image.Metadata.XmpProfile = original; + var encoder = new JpegEncoder(); + + // act + using Image reloadedImage = WriteAndRead(image, encoder); + + // assert + XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile; + XmpProfileContainsExpectedValues(actual); + Assert.Equal(original.Data, actual.Data); + } + + [Fact] + public async void WritingJpeg_PreservesExtendedXmpProfile() + { + // arrange + var provider = TestImageProvider.File(TestImages.Jpeg.Baseline.ExtendedXmp); + using Image image = await provider.GetImageAsync(JpegDecoder); + XmpProfile original = image.Metadata.XmpProfile; + var encoder = new JpegEncoder(); + + // act + using Image reloadedImage = WriteAndRead(image, encoder); + + // assert + XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile; + XmpProfileContainsExpectedValues(actual); + Assert.Equal(original.Data, actual.Data); + } + + [Fact] + public void WritingPng_PreservesXmpProfile() + { + // arrange + var image = new Image(1, 1); + XmpProfile original = CreateMinimalXmlProfile(); + image.Metadata.XmpProfile = original; + var encoder = new PngEncoder(); + + // act + using Image reloadedImage = WriteAndRead(image, encoder); + + // assert + XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile; + XmpProfileContainsExpectedValues(actual); + Assert.Equal(original.Data, actual.Data); + } + + [Fact] + public void WritingTiff_PreservesXmpProfile() + { + // arrange + var image = new Image(1, 1); + XmpProfile original = CreateMinimalXmlProfile(); + image.Frames.RootFrame.Metadata.XmpProfile = original; + var encoder = new TiffEncoder(); + + // act + using Image reloadedImage = WriteAndRead(image, encoder); + + // assert + XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile; + XmpProfileContainsExpectedValues(actual); + Assert.Equal(original.Data, actual.Data); + } + + [Fact] + public void WritingWebp_PreservesXmpProfile() + { + // arrange + var image = new Image(1, 1); + XmpProfile original = CreateMinimalXmlProfile(); + image.Metadata.XmpProfile = original; + var encoder = new WebpEncoder(); + + // act + using Image reloadedImage = WriteAndRead(image, encoder); + + // assert + XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile; + XmpProfileContainsExpectedValues(actual); + Assert.Equal(original.Data, actual.Data); + } + + private static void XmpProfileContainsExpectedValues(XmpProfile xmp) + { + Assert.NotNull(xmp); + XDocument document = xmp.GetDocument(); + Assert.NotNull(document); + Assert.Equal("xmpmeta", document.Root.Name.LocalName); + Assert.Equal("adobe:ns:meta/", document.Root.Name.NamespaceName); + } + + private static XmpProfile CreateMinimalXmlProfile() + { + string content = $" "; + byte[] data = Encoding.UTF8.GetBytes(content); + var profile = new XmpProfile(data); + return profile; + } + + private static Image WriteAndRead(Image image, IImageEncoder encoder) + { + using (var memStream = new MemoryStream()) + { + image.Save(memStream, encoder); + image.Dispose(); + + memStream.Position = 0; + return Image.Load(memStream); + } + } + } +} diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/AffineTransformTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/AffineTransformTests.cs index 3cd8cec154..33725f7aa5 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/AffineTransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/AffineTransformTests.cs @@ -4,6 +4,7 @@ using System; using System.Numerics; using System.Reflection; +using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Transforms; @@ -224,6 +225,46 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms c => c.Transform(builder)); } + [Fact] + public void Issue1911() + { + using var image = new Image(100, 100); + image.Mutate(x => x = x.Transform(new Rectangle(0, 0, 99, 100), Matrix3x2.Identity, new Size(99, 100), KnownResamplers.Lanczos2)); + + Assert.Equal(99, image.Width); + Assert.Equal(100, image.Height); + } + + [Theory] + [WithTestPatternImages(100, 100, PixelTypes.Rgba32)] + public void Identity(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + + Matrix3x2 m = Matrix3x2.Identity; + Rectangle r = new(25, 25, 50, 50); + image.Mutate(x => x.Transform(r, m, new Size(100, 100), KnownResamplers.Bicubic)); + image.DebugSave(provider); + image.CompareToReferenceOutput(ValidatorComparer, provider); + } + + [Theory] + [WithTestPatternImages(100, 100, PixelTypes.Rgba32, 0.0001F)] + [WithTestPatternImages(100, 100, PixelTypes.Rgba32, 57F)] + [WithTestPatternImages(100, 100, PixelTypes.Rgba32, 0F)] + public void Transform_With_Custom_Dimensions(TestImageProvider provider, float radians) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + + var m = Matrix3x2.CreateRotation(radians, new Vector2(50, 50)); + Rectangle r = new(25, 25, 50, 50); + image.Mutate(x => x.Transform(r, m, new Size(100, 100), KnownResamplers.Bicubic)); + image.DebugSave(provider, testOutputDetails: radians); + image.CompareToReferenceOutput(ValidatorComparer, provider, testOutputDetails: radians); + } + private static IResampler GetResampler(string name) { PropertyInfo property = typeof(KnownResamplers).GetTypeInfo().GetProperty(name); diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/AutoOrientTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/AutoOrientTests.cs index 38fde5060a..597c02dfc2 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/ResizeTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs index 022bb224c4..6e275b824e 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs @@ -669,5 +669,17 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms Assert.Equal(height, image.Height); } } + + [Theory] + [WithBasicTestPatternImages(20, 20, PixelTypes.Rgba32)] + public void Issue1625_LimitedAllocator(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + provider.LimitAllocatorBufferCapacity().InBytes(1000); + provider.RunValidatingProcessorTest( + x => x.Resize(30, 30), + appendPixelTypeToFileName: false, + appendSourceFileOrDescription: false); + } } } diff --git a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs index ef8e03763c..d841c963f6 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs @@ -147,6 +147,46 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms } } + [Fact] + public void Issue1911() + { + using var image = new Image(100, 100); + image.Mutate(x => x = x.Transform(new Rectangle(0, 0, 99, 100), Matrix4x4.Identity, new Size(99, 100), KnownResamplers.Lanczos2)); + + Assert.Equal(99, image.Width); + Assert.Equal(100, image.Height); + } + + [Theory] + [WithTestPatternImages(100, 100, PixelTypes.Rgba32)] + public void Identity(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + + Matrix4x4 m = Matrix4x4.Identity; + Rectangle r = new(25, 25, 50, 50); + image.Mutate(x => x.Transform(r, m, new Size(100, 100), KnownResamplers.Bicubic)); + image.DebugSave(provider); + image.CompareToReferenceOutput(ValidatorComparer, provider); + } + + [Theory] + [WithTestPatternImages(100, 100, PixelTypes.Rgba32, 0.0001F)] + [WithTestPatternImages(100, 100, PixelTypes.Rgba32, 57F)] + [WithTestPatternImages(100, 100, PixelTypes.Rgba32, 0F)] + public void Transform_With_Custom_Dimensions(TestImageProvider provider, float radians) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + + Matrix4x4 m = Matrix4x4.CreateRotationX(radians, new Vector3(50, 50, 1F)) * Matrix4x4.CreateRotationY(radians, new Vector3(50, 50, 1F)); + Rectangle r = new(25, 25, 50, 50); + image.Mutate(x => x.Transform(r, m, new Size(100, 100), KnownResamplers.Bicubic)); + image.DebugSave(provider, testOutputDetails: radians); + image.CompareToReferenceOutput(ValidatorComparer, provider, testOutputDetails: radians); + } + private static IResampler GetResampler(string name) { PropertyInfo property = typeof(KnownResamplers).GetTypeInfo().GetProperty(name); diff --git a/tests/ImageSharp.Tests/Quantization/PixelSamplingStrategyTests.cs b/tests/ImageSharp.Tests/Quantization/PixelSamplingStrategyTests.cs index 9a8d8351b2..7529b0e5f4 100644 --- a/tests/ImageSharp.Tests/Quantization/PixelSamplingStrategyTests.cs +++ b/tests/ImageSharp.Tests/Quantization/PixelSamplingStrategyTests.cs @@ -75,7 +75,7 @@ namespace SixLabors.ImageSharp.Tests.Quantization var white = new L8(255); for (int y = 0; y < region.Height; y++) { - region.GetRowSpan(y).Fill(white); + region.DangerousGetRowSpan(y).Fill(white); } } diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index b87ffc7420..8b943194a5 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"; @@ -61,6 +62,7 @@ namespace SixLabors.ImageSharp.Tests public const string David = "Png/david.png"; public const string TestPattern31x31 = "Png/testpattern31x31.png"; public const string TestPattern31x31HalfTransparent = "Png/testpattern31x31-halftransparent.png"; + public const string XmpColorPalette = "Png/xmp-colorpalette.png"; // Filtered test images from http://www.schaik.com/pngsuite/pngsuite_fil_png.html public const string Filter0 = "Png/filter0.png"; @@ -117,6 +119,9 @@ namespace SixLabors.ImageSharp.Tests // Issue 1765: https://github.com/SixLabors/ImageSharp/issues/1765 public const string Issue1765_Net6DeflateStreamRead = "Png/issues/Issue_1765_Net6DeflateStreamRead.png"; + // Discussion 1875: https://github.com/SixLabors/ImageSharp/discussions/1875 + public const string Issue1875 = "Png/raw-profile-type-exif.png"; + public static class Bad { public const string MissingDataChunk = "Png/xdtn0g01.png"; @@ -187,6 +192,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,6 +221,9 @@ namespace SixLabors.ImageSharp.Tests 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"; public static readonly string[] All = { @@ -253,6 +262,9 @@ namespace SixLabors.ImageSharp.Tests public const string BadSubSampling1076 = "Jpg/issues/issue-1076-invalid-subsampling.jpg"; public const string IdentifyMultiFrame1211 = "Jpg/issues/issue-1221-identify-multi-frame.jpg"; public const string WrongColorSpace = "Jpg/issues/Issue1732-WrongColorSpace.jpg"; + 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 static class Fuzz { @@ -439,6 +451,7 @@ namespace SixLabors.ImageSharp.Tests 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 static readonly string[] All = { Rings, Giphy, Cheers, Trans, Kumin, Leo, Ratio4x1, Ratio1x4 }; @@ -621,6 +634,7 @@ namespace SixLabors.ImageSharp.Tests public const string Earth = "Webp/earth_lossy.webp"; public const string WithExif = "Webp/exif_lossy.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"; // Lossy images without macroblock filtering. @@ -869,6 +883,29 @@ namespace SixLabors.ImageSharp.Tests public static readonly string[] Metadata = { SampleMetadata }; } + public static class BigTiff + { + public const string Base = "Tiff/BigTiff/"; + + public const string BigTIFF = Base + "BigTIFF.tif"; + public const string BigTIFFLong = Base + "BigTIFFLong.tif"; + public const string BigTIFFLong8 = Base + "BigTIFFLong8.tif"; + public const string BigTIFFLong8Tiles = Base + "BigTIFFLong8Tiles.tif"; + public const string BigTIFFMotorola = Base + "BigTIFFMotorola.tif"; + public const string BigTIFFMotorolaLongStrips = Base + "BigTIFFMotorolaLongStrips.tif"; + + public const string BigTIFFSubIFD4 = Base + "BigTIFFSubIFD4.tif"; + public const string BigTIFFSubIFD8 = Base + "BigTIFFSubIFD8.tif"; + + public const string Indexed4_Deflate = Base + "BigTIFF_Indexed4_Deflate.tif"; + public const string Indexed8_LZW = Base + "BigTIFF_Indexed8_LZW.tif"; + public const string MinIsBlack = Base + "BigTIFF_MinIsBlack.tif"; + public const string MinIsWhite = Base + "BigTIFF_MinIsWhite.tif"; + + public const string Damaged_MinIsWhite_RLE = Base + "BigTIFF_MinIsWhite_RLE.tif"; + public const string Damaged_MinIsBlack_RLE = Base + "BigTIFF_MinIsBlack_RLE.tif"; + } + public static class Pbm { public const string BlackAndWhitePlain = "Pbm/blackandwhite_plain.pbm"; diff --git a/tests/Images/External/README.md b/tests/Images/External/README.md index 936b15e6a8..9e9fcf5e1c 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/AffineTransformTests/Identity_Rgba32_TestPattern100x100.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Identity_Rgba32_TestPattern100x100.png new file mode 100644 index 0000000000..ce6e8ce9fa --- /dev/null +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Identity_Rgba32_TestPattern100x100.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da8229605bda413676a42f587df250a743540e6e00c04eacb1e622f223e19595 +size 3564 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_FromSourceRectangle1_Rgba32_TestPattern96x48.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_FromSourceRectangle1_Rgba32_TestPattern96x48.png index f5af011026..53ac0ff89f 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_FromSourceRectangle1_Rgba32_TestPattern96x48.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_FromSourceRectangle1_Rgba32_TestPattern96x48.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b01d54838d678b61c3b7a1c7e76ff9a60b3f5f4faef5af848231177eac956eb1 -size 1262 +oid sha256:bbe1ffaf7b801fd92724438cc810fd0c5506e0a907b970c4f0bf5bec3627ca2a +size 551 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_FromSourceRectangle2_Rgba32_TestPattern96x48.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_FromSourceRectangle2_Rgba32_TestPattern96x48.png index e5005ac5d4..2480164d60 100644 --- a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_FromSourceRectangle2_Rgba32_TestPattern96x48.png +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_FromSourceRectangle2_Rgba32_TestPattern96x48.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d34394771605c2a70cc23f3841592c20d22a68aaabf2ad6e8aba7348a181afb3 -size 531 +oid sha256:b45933471a1af1b6d4112240e1bc6b6187065a872043ddbf917200ce9e8cc84b +size 371 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.0001.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.0001.png new file mode 100644 index 0000000000..4b9953b670 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.0001.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dd3b29b530e221618f65cd5e493b21fe3c27804fde7664636b7bb002f72abbb2 +size 3663 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.png new file mode 100644 index 0000000000..ce6e8ce9fa --- /dev/null +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da8229605bda413676a42f587df250a743540e6e00c04eacb1e622f223e19595 +size 3564 diff --git a/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_57.png b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_57.png new file mode 100644 index 0000000000..5f4911e47c --- /dev/null +++ b/tests/Images/External/ReferenceOutput/AffineTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_57.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a35757fef08a6fd9b37e719d5be7a82d5ff79f0395e082f697d9ebe9c7f03cc8 +size 5748 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 0000000000..24f5e9c0cd --- /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/JpegDecoderTests/DecodeBaselineJpeg_grayscale_sampling22.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/DecodeBaselineJpeg_grayscale_sampling22.png new file mode 100644 index 0000000000..b2c3effdda --- /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/ProjectiveTransformTests/Identity_Rgba32_TestPattern100x100.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Identity_Rgba32_TestPattern100x100.png new file mode 100644 index 0000000000..ce6e8ce9fa --- /dev/null +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Identity_Rgba32_TestPattern100x100.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da8229605bda413676a42f587df250a743540e6e00c04eacb1e622f223e19595 +size 3564 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.0001.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.0001.png new file mode 100644 index 0000000000..e8efa8a980 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.0001.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:39c25539c3c9b8926bf65c041df693a60617bbe8653bb72357bde5ab6342c59c +size 3618 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.png new file mode 100644 index 0000000000..ce6e8ce9fa --- /dev/null +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da8229605bda413676a42f587df250a743540e6e00c04eacb1e622f223e19595 +size 3564 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_57.png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_57.png new file mode 100644 index 0000000000..99a74e400a --- /dev/null +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_With_Custom_Dimensions_Rgba32_TestPattern100x100_57.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8b1fc95fdf07c7443147205afffb157aa82f94818cfbb833a615c42f584fbda0 +size 5070 diff --git a/tests/Images/External/ReferenceOutput/ResizeTests/Issue1625_LimitedAllocator.png b/tests/Images/External/ReferenceOutput/ResizeTests/Issue1625_LimitedAllocator.png new file mode 100644 index 0000000000..01e9cf38f3 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/ResizeTests/Issue1625_LimitedAllocator.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bedaced9c302ff735319f189d867b6a722ed4eade63152b67cf07881f8b3964d +size 289 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 0000000000..a5bc432d8b --- /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/Jpg/baseline/Calliphora_encoded_strings.jpg b/tests/Images/Input/Jpg/baseline/Calliphora_encoded_strings.jpg new file mode 100644 index 0000000000..b652ed2e58 --- /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/Metadata-test-file.jpg b/tests/Images/Input/Jpg/baseline/Metadata-test-file.jpg new file mode 100644 index 0000000000..160d7ebf81 --- /dev/null +++ b/tests/Images/Input/Jpg/baseline/Metadata-test-file.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:800911efb6f0c796d61b5ea14fc67fe891aaae3c04a49cfd5b86e68958598436 +size 138810 diff --git a/tests/Images/Input/Jpg/baseline/extended-xmp.jpg b/tests/Images/Input/Jpg/baseline/extended-xmp.jpg new file mode 100644 index 0000000000..6fc84b95eb --- /dev/null +++ b/tests/Images/Input/Jpg/baseline/extended-xmp.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:000c67f210059b101570949e889846bb11d6bdc801bd641d7d26424ad9cd027f +size 623986 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 0000000000..b861c68ab5 --- /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/Issue1942InvalidIptcTag.jpg b/tests/Images/Input/Jpg/issues/Issue1942InvalidIptcTag.jpg new file mode 100644 index 0000000000..8b1926128c --- /dev/null +++ b/tests/Images/Input/Jpg/issues/Issue1942InvalidIptcTag.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9c9db428c4d9d7d1aea6778f263d8deaeeabdcfa63c77ef6ce36ab0e47b364dd +size 93374 diff --git a/tests/Images/Input/Jpg/issues/issue-1900-malformed-unsupported-255-components.jpg b/tests/Images/Input/Jpg/issues/issue-1900-malformed-unsupported-255-components.jpg new file mode 100644 index 0000000000..7b6d7c5728 --- /dev/null +++ b/tests/Images/Input/Jpg/issues/issue-1900-malformed-unsupported-255-components.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3553f76b72b98986df13a7508cd073a30b7c846dc441d1d68a518bf7b93bc66 +size 3951 diff --git a/tests/Images/Input/Jpg/issues/issue-1932-app0-resolution.jpg b/tests/Images/Input/Jpg/issues/issue-1932-app0-resolution.jpg new file mode 100644 index 0000000000..7f14e808e5 --- /dev/null +++ b/tests/Images/Input/Jpg/issues/issue-1932-app0-resolution.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3654c48003b85c1110bad8c31d2f94eaf4dcfe488698246b3ead4b54715d8d18 +size 1325 diff --git a/tests/Images/Input/Png/raw-profile-type-exif.png b/tests/Images/Input/Png/raw-profile-type-exif.png new file mode 100644 index 0000000000..efd9b35aaa --- /dev/null +++ b/tests/Images/Input/Png/raw-profile-type-exif.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2259b08fd0c4681ecd068244df358b486f5eca1fcd18edbc7d9207eeef3ca5ed +size 392 diff --git a/tests/Images/Input/Png/transparency.png b/tests/Images/Input/Png/transparency.png new file mode 100644 index 0000000000..26de0f2d1a --- /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/Png/xmp-colorpalette.png b/tests/Images/Input/Png/xmp-colorpalette.png new file mode 100644 index 0000000000..375879413b --- /dev/null +++ b/tests/Images/Input/Png/xmp-colorpalette.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb55607fd7de6a47d8dd242c1a7be9627c564821554db896ed46603d15963c06 +size 1025 diff --git a/tests/Images/Input/Tiff/BigTiff/BigTIFF.tif b/tests/Images/Input/Tiff/BigTiff/BigTIFF.tif new file mode 100644 index 0000000000..b278344796 --- /dev/null +++ b/tests/Images/Input/Tiff/BigTiff/BigTIFF.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ddb202145a9bce7670cc372ee578de5a53cd52cc8d5ae8a9ebdc9f9c4f4a7e81 +size 12480 diff --git a/tests/Images/Input/Tiff/BigTiff/BigTIFFLong.tif b/tests/Images/Input/Tiff/BigTiff/BigTIFFLong.tif new file mode 100644 index 0000000000..b390c0a97f --- /dev/null +++ b/tests/Images/Input/Tiff/BigTiff/BigTIFFLong.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:90178643a159ec50335e9314836df924233debeb100763af0f77cd1be3cf58ab +size 12480 diff --git a/tests/Images/Input/Tiff/BigTiff/BigTIFFLong8.tif b/tests/Images/Input/Tiff/BigTiff/BigTIFFLong8.tif new file mode 100644 index 0000000000..e1815c7501 --- /dev/null +++ b/tests/Images/Input/Tiff/BigTiff/BigTIFFLong8.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b38e61ccb01e10e26fb10c335fc6fca9ad087b0fb0df833e54bb02d1246e20d5 +size 12480 diff --git a/tests/Images/Input/Tiff/BigTiff/BigTIFFLong8Tiles.tif b/tests/Images/Input/Tiff/BigTiff/BigTIFFLong8Tiles.tif new file mode 100644 index 0000000000..c69218948a --- /dev/null +++ b/tests/Images/Input/Tiff/BigTiff/BigTIFFLong8Tiles.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2d92b0c430cefc390f13961e00950ee7246b013335594dd249ba823eb3c3fdb +size 12564 diff --git a/tests/Images/Input/Tiff/BigTiff/BigTIFFMotorola.tif b/tests/Images/Input/Tiff/BigTiff/BigTIFFMotorola.tif new file mode 100644 index 0000000000..68c2b3a417 --- /dev/null +++ b/tests/Images/Input/Tiff/BigTiff/BigTIFFMotorola.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ace8a27dbed9f918993615e545a12310b84ad94bc6af8e256258e69731f1c7ce +size 12480 diff --git a/tests/Images/Input/Tiff/BigTiff/BigTIFFMotorolaLongStrips.tif b/tests/Images/Input/Tiff/BigTiff/BigTIFFMotorolaLongStrips.tif new file mode 100644 index 0000000000..cc62e0362f --- /dev/null +++ b/tests/Images/Input/Tiff/BigTiff/BigTIFFMotorolaLongStrips.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c5ebdc3774955d3b47644f57bd023e764a93ca2271118152ae920b34c1784bb +size 12480 diff --git a/tests/Images/Input/Tiff/BigTiff/BigTIFFSamples.md b/tests/Images/Input/Tiff/BigTiff/BigTIFFSamples.md new file mode 100644 index 0000000000..0401109fdd --- /dev/null +++ b/tests/Images/Input/Tiff/BigTiff/BigTIFFSamples.md @@ -0,0 +1,220 @@ +These images were created by [AWare Systems](http://www.awaresystems.be/). + +# Index + +[Classic.tif](#classictif) +[BigTIFF.tif](#bigtifftif) +[BigTIFFMotorola.tif](#bigtiffmotorolatif) +[BigTIFFLong.tif](#bigtifflongtif) +[BigTIFFLong8.tif](#bigtifflong8tif) +[BigTIFFMotorolaLongStrips.tif](#bigtiffmotorolalongstripstif) +[BigTIFFLong8Tiles.tif](#bigtifflong8tilestif) +[BigTIFFSubIFD4.tif](#bigtiffsubifd4tif) +[BigTIFFSubIFD8.tif](#bigtiffsubifd8tif) + +# Classic.tif + +Classic.tif is a basic Classic TIFF file. All files in this package have the same actual image content, so this TIFF file serves as a reference. + +Format: Classic TIFF +Byte Order: Intel +Ifd Offset: 12302 +    ImageWidth (1 Short): 64 +    ImageLength (1 Short): 64 +    BitsPerSample (3 Short): 8, 8, 8 +    PhotometricInterpretation (1 Short): RGB +    StripOffsets (1 Short): 8 +    SamplesPerPixel (1 Short): 3 +    RowsPerStrip (1 Short): 64 +    StripByteCounts (1 Short): 12288 + +# BigTIFF.tif + +BigTIFF.tif ressembles Classic.tif as close as possible. Except that it's a BigTIFF, that is... + +Format: BigTIFF +Byte Order: Intel +Ifd Offset: 12304 +    ImageWidth (1 Short): 64 +    ImageLength (1 Short): 64 +    BitsPerSample (3 Short): 8, 8, 8 +    PhotometricInterpretation (1 Short): RGB +    StripOffsets (1 Short): 16 +    SamplesPerPixel (1 Short): 3 +    RowsPerStrip (1 Short): 64 +    StripByteCounts (1 Short): 12288 + +# BigTIFFMotorola.tif + +BigTIFFMotorola.tif reverses the byte order. + +Format: BigTIFF +Byte Order: Motorola +Ifd Offset: 12304 +    ImageWidth (1 Short): 64 +    ImageLength (1 Short): 64 +    BitsPerSample (3 Short): 8, 8, 8 +    PhotometricInterpretation (1 Short): RGB +    StripOffsets (1 Short): 16 +    SamplesPerPixel (1 Short): 3 +    RowsPerStrip (1 Short): 64 +    StripByteCounts (1 Short): 12288 + +# BigTIFFLong.tif + +All previous TIFFs specify DataType Short for StripOffsets and StripByteCounts tags. This BigTIFF instead specifies DataType Long, for these tags. + +Format: BigTIFF +Byte Order: Intel +Ifd Offset: 12304 +    ImageWidth (1 Short): 64 +    ImageLength (1 Short): 64 +    BitsPerSample (3 Short): 8, 8, 8 +    PhotometricInterpretation (1 Short): RGB +    StripOffsets (1 Long): 16 +    SamplesPerPixel (1 Short): 3 +    RowsPerStrip (1 Short): 64 +    StripByteCounts (1 Long): 12288 + +# BigTIFFLong8.tif + +This next one specifies DataType Long8, for StripOffsets and StripByteCounts tags. + +Format: BigTIFF +Byte Order: Intel +Ifd Offset: 12304 +    ImageWidth (1 Short): 64 +    ImageLength (1 Short): 64 +    BitsPerSample (3 Short): 8, 8, 8 +    PhotometricInterpretation (1 Short): RGB +    StripOffsets (1 Long8): 16 +    SamplesPerPixel (1 Short): 3 +    RowsPerStrip (1 Short): 64 +    StripByteCounts (1 Long8): 12288 + +# BigTIFFMotorolaLongStrips.tif + +This BigTIFF has Motorola byte order, plus, it's divided over two strips. StripOffsets and StripByteCounts tags have DataType Long, so their actual value fits inside the IFD. + +Format: BigTIFF +Byte Order: Motorola +Ifd Offset: 12304 +    ImageWidth (1 Short): 64 +    ImageLength (1 Short): 64 +    BitsPerSample (3 Short): 8, 8, 8 +    PhotometricInterpretation (1 Short): RGB +    StripOffsets (2 Long): 16, 6160 +    SamplesPerPixel (1 Short): 3 +    RowsPerStrip (1 Short): 32 +    StripByteCounts (2 Long): 6144, 6144 + +# BigTIFFLong8Tiles.tif + +BigTIFFLong8Tiles.tif is a tiled BigTIFF. TileOffsets and TileByteCounts tags specify DataType Long8. + +Format: BigTIFF +Byte Order: Intel +Ifd Offset: 12368 +    ImageWidth (1 Short): 64 +    ImageLength (1 Short): 64 +    BitsPerSample (3 Short): 8, 8, 8 +    PhotometricInterpretation (1 Short): RGB +    SamplesPerPixel (1 Short): 3 +    TileWidth (1 Short): 32 +    TileLength (1 Short): 32 +    TileOffsets (4 Long8): 16, 3088, 6160, 9232 +    TileByteCounts (4 Long8): 3072, 3072, 3072, 3072 + +# BigTIFFSubIFD4.tif + +This BigTIFF contains two pages, the second page showing almost the same image content as the first, except that the black square is white, and text color is black. Both pages point to a downsample SubIFD, using SubIFDs DataType TIFF_IFD. + +Format: BigTIFF +Byte Order: Intel +Ifd Offset: 15572 +    ImageWidth (1 Short): 64 +    ImageLength (1 Short): 64 +    BitsPerSample (3 Short): 8, 8, 8 +    PhotometricInterpretation (1 Short): RGB +    StripOffsets (1 Short): 3284 +    SamplesPerPixel (1 Short): 3 +    RowsPerStrip (1 Short): 64 +    StripByteCounts (1 Short): 12288 +    SubIFDs (1 IFD): 3088 +SubIfd Offset: 3088 +    NewSubFileType (1 Long): 1 +    ImageWidth (1 Short): 32 +    ImageLength (1 Short): 32 +    BitsPerSample (3 Short): 8, 8, 8 +    PhotometricInterpretation (1 Short): RGB +    StripOffsets (1 Short): 16 +    SamplesPerPixel (1 Short): 3 +    RowsPerStrip (1 Short): 32 +    StripByteCounts (1 Short): 3072 +Ifd Offset: 31324 +    ImageWidth (1 Short): 64 +    ImageLength (1 Short): 64 +    BitsPerSample (3 Short): 8, 8, 8 +    PhotometricInterpretation (1 Short): RGB +    StripOffsets (1 Short): 19036 +    SamplesPerPixel (1 Short): 3 +    RowsPerStrip (1 Short): 64 +    StripByteCounts (1 Short): 12288 +    SubIFDs (1 IFD): 18840 +SubIfd Offset: 18840 +    NewSubFileType (1 Long): 1 +    ImageWidth (1 Short): 32 +    ImageLength (1 Short): 32 +    BitsPerSample (3 Short): 8, 8, 8 +    PhotometricInterpretation (1 Short): RGB +    StripOffsets (1 Short): 15768 +    SamplesPerPixel (1 Short): 3 +    RowsPerStrip (1 Short): 32 +    StripByteCounts (1 Short): 3072 + +# BigTIFFSubIFD8.tif + +BigTIFFSubIFD4.tif is very much the same as BigTIFFSubIFD4.tif, except that the new DataType TIFF_IFD8 is used for the SubIFDs tag. + +Format: BigTIFF +Byte Order: Intel +Ifd Offset: 15572 +    ImageWidth (1 Short): 64 +    ImageLength (1 Short): 64 +    BitsPerSample (3 Short): 8, 8, 8 +    PhotometricInterpretation (1 Short): RGB +    StripOffsets (1 Short): 3284 +    SamplesPerPixel (1 Short): 3 +    RowsPerStrip (1 Short): 64 +    StripByteCounts (1 Short): 12288 +    SubIFDs (1 IFD8): 3088 +SubIfd Offset: 3088 +    NewSubFileType (1 Long): 1 +    ImageWidth (1 Short): 32 +    ImageLength (1 Short): 32 +    BitsPerSample (3 Short): 8, 8, 8 +    PhotometricInterpretation (1 Short): RGB +    StripOffsets (1 Short): 16 +    SamplesPerPixel (1 Short): 3 +    RowsPerStrip (1 Short): 32 +    StripByteCounts (1 Short): 3072 +Ifd Offset: 31324 +    ImageWidth (1 Short): 64 +    ImageLength (1 Short): 64 +    BitsPerSample (3 Short): 8, 8, 8 +    PhotometricInterpretation (1 Short): RGB +    StripOffsets (1 Short): 19036 +    SamplesPerPixel (1 Short): 3 +    RowsPerStrip (1 Short): 64 +    StripByteCounts (1 Short): 12288 +    SubIFDs (1 IFD8): 18840 +SubIfd Offset: 18840 +    NewSubFileType (1 Long): 1 +    ImageWidth (1 Short): 32 +    ImageLength (1 Short): 32 +    BitsPerSample (3 Short): 8, 8, 8 +    PhotometricInterpretation (1 Short): RGB +    StripOffsets (1 Short): 15768 +    SamplesPerPixel (1 Short): 3 +    RowsPerStrip (1 Short): 32 +    StripByteCounts (1 Short): 3072 \ No newline at end of file diff --git a/tests/Images/Input/Tiff/BigTiff/BigTIFFSubIFD4.tif b/tests/Images/Input/Tiff/BigTiff/BigTIFFSubIFD4.tif new file mode 100644 index 0000000000..cd7bdf0b59 --- /dev/null +++ b/tests/Images/Input/Tiff/BigTiff/BigTIFFSubIFD4.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:85c8da46abc2284f0ddac10bd03a7c98ee51024840b7e43f41f1c6a09bc02e7e +size 31520 diff --git a/tests/Images/Input/Tiff/BigTiff/BigTIFFSubIFD8.tif b/tests/Images/Input/Tiff/BigTiff/BigTIFFSubIFD8.tif new file mode 100644 index 0000000000..bb0b3bf189 --- /dev/null +++ b/tests/Images/Input/Tiff/BigTiff/BigTIFFSubIFD8.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:93e5ac30f507bec7936746ad6e109631c09f9b2332081e986063219ad2452a4a +size 31520 diff --git a/tests/Images/Input/Tiff/BigTiff/BigTIFF_Indexed4_Deflate.tif b/tests/Images/Input/Tiff/BigTiff/BigTIFF_Indexed4_Deflate.tif new file mode 100644 index 0000000000..bc736790a0 --- /dev/null +++ b/tests/Images/Input/Tiff/BigTiff/BigTIFF_Indexed4_Deflate.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:38ba3717b284d7914243609576d0f9b75d732692bf05e2e1ec8b119feb1409fd +size 687 diff --git a/tests/Images/Input/Tiff/BigTiff/BigTIFF_Indexed8_LZW.tif b/tests/Images/Input/Tiff/BigTiff/BigTIFF_Indexed8_LZW.tif new file mode 100644 index 0000000000..edff7c40ad --- /dev/null +++ b/tests/Images/Input/Tiff/BigTiff/BigTIFF_Indexed8_LZW.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ca404b3ec5560b82169855f0ae69e64c6bc7286117b95fc0e0d505e5e356fa0e +size 2548 diff --git a/tests/Images/Input/Tiff/BigTiff/BigTIFF_MinIsBlack.tif b/tests/Images/Input/Tiff/BigTiff/BigTIFF_MinIsBlack.tif new file mode 100644 index 0000000000..491b4092e7 --- /dev/null +++ b/tests/Images/Input/Tiff/BigTiff/BigTIFF_MinIsBlack.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ac0471c1600f6e5fb47037dab07172aff524abc866a40c9ec54279bd49cbef77 +size 517 diff --git a/tests/Images/Input/Tiff/BigTiff/BigTIFF_MinIsBlack_RLE.tif b/tests/Images/Input/Tiff/BigTiff/BigTIFF_MinIsBlack_RLE.tif new file mode 100644 index 0000000000..d15188c174 --- /dev/null +++ b/tests/Images/Input/Tiff/BigTiff/BigTIFF_MinIsBlack_RLE.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:038a298bcace02810054af650f490b6858863c8755e41b786605aa807b43350a +size 509 diff --git a/tests/Images/Input/Tiff/BigTiff/BigTIFF_MinIsWhite.tif b/tests/Images/Input/Tiff/BigTiff/BigTIFF_MinIsWhite.tif new file mode 100644 index 0000000000..fb26e58a4c --- /dev/null +++ b/tests/Images/Input/Tiff/BigTiff/BigTIFF_MinIsWhite.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a95b0b46bf4f75babb86d9ec74694e6d684087504be214df48a6c8a54338834c +size 517 diff --git a/tests/Images/Input/Tiff/BigTiff/BigTIFF_MinIsWhite_RLE.tif b/tests/Images/Input/Tiff/BigTiff/BigTIFF_MinIsWhite_RLE.tif new file mode 100644 index 0000000000..d15188c174 --- /dev/null +++ b/tests/Images/Input/Tiff/BigTiff/BigTIFF_MinIsWhite_RLE.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:038a298bcace02810054af650f490b6858863c8755e41b786605aa807b43350a +size 509 diff --git a/tests/Images/Input/Tiff/BigTiff/Classic.tif b/tests/Images/Input/Tiff/BigTiff/Classic.tif new file mode 100644 index 0000000000..1fa6be78b7 --- /dev/null +++ b/tests/Images/Input/Tiff/BigTiff/Classic.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cfa7fcf6927be5de644beb238067479e8d834d0cbe2257b00302f5dde84a1c1a +size 12404 diff --git a/tests/Images/Input/Tiff/BigTiff/readme.md b/tests/Images/Input/Tiff/BigTiff/readme.md new file mode 100644 index 0000000000..29f9c8ea9e --- /dev/null +++ b/tests/Images/Input/Tiff/BigTiff/readme.md @@ -0,0 +1,5 @@ +#### BigTIFF samples. + +For details: [BigTIFFSamples.md](BigTIFFSamples.md) + +Downloaded from https://www.awaresystems.be/imaging/tiff/bigtiff.html \ No newline at end of file diff --git a/tests/Images/Input/Webp/xmp_lossy.webp b/tests/Images/Input/Webp/xmp_lossy.webp new file mode 100644 index 0000000000..4e92f280c3 --- /dev/null +++ b/tests/Images/Input/Webp/xmp_lossy.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:755a63652695d7e190f375c9c0697cd37c9b601cd54405c704ec8efc200e67fc +size 474772