Browse Source

Merge remote-tracking branch 'origin/main' into bp/openExr

# Conflicts:
#	src/ImageSharp/Configuration.cs
pull/3096/head
Brian Popow 4 years ago
parent
commit
88a06e8b06
  1. 2
      .github/CONTRIBUTING.md
  2. 33
      .github/ISSUE_TEMPLATE/commercial-bug-report.md
  3. 42
      .github/ISSUE_TEMPLATE/commercial-bug-report.yml
  4. 30
      .github/ISSUE_TEMPLATE/oss-bug-report.md
  5. 40
      .github/ISSUE_TEMPLATE/oss-bug-report.yml
  6. 392
      .github/workflows/build-and-test.yml
  7. 81
      .github/workflows/code-coverage.yml
  8. 1
      ImageSharp.sln
  9. 12
      README.md
  10. 98
      src/ImageSharp/Common/Helpers/HexConverter.cs
  11. 12
      src/ImageSharp/Configuration.cs
  12. 83
      src/ImageSharp/Diagnostics/MemoryDiagnostics.cs
  13. 11
      src/ImageSharp/Formats/Gif/GifConstants.cs
  14. 78
      src/ImageSharp/Formats/Gif/GifDecoderCore.cs
  15. 47
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  16. 4
      src/ImageSharp/Formats/Gif/Sections/GifGraphicControlExtension.cs
  17. 2
      src/ImageSharp/Formats/Gif/Sections/GifNetscapeLoopingApplicationExtension.cs
  18. 97
      src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs
  19. 7
      src/ImageSharp/Formats/Gif/Sections/IGifExtension.cs
  20. 8
      src/ImageSharp/Formats/ImageExtensions.Save.cs
  21. 1
      src/ImageSharp/Formats/ImageExtensions.Save.tt
  22. 26
      src/ImageSharp/Formats/ImageFormatManager.cs
  23. 8
      src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs
  24. 103
      src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs
  25. 76
      src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs
  26. 10
      src/ImageSharp/Formats/Jpeg/Components/Decoder/JFifMarker.cs
  27. 12
      src/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs
  28. 5
      src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs
  29. 5
      src/ImageSharp/Formats/Jpeg/Components/FastFloatingPointDCT.Intrinsic.cs
  30. 139
      src/ImageSharp/Formats/Jpeg/Components/FastFloatingPointDCT.cs
  31. 429
      src/ImageSharp/Formats/Jpeg/Components/ZigZag.Intrinsic.cs
  32. 176
      src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
  33. 57
      src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
  34. 3
      src/ImageSharp/Formats/Jpeg/JpegThrowHelper.cs
  35. 13
      src/ImageSharp/Formats/PixelTypeInfo.cs
  36. 24
      src/ImageSharp/Formats/Png/PngConstants.cs
  37. 218
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  38. 115
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  39. 468
      src/ImageSharp/Formats/Tiff/Compression/Compressors/T4BitCompressor.cs
  40. 201
      src/ImageSharp/Formats/Tiff/Compression/Compressors/T6BitCompressor.cs
  41. 536
      src/ImageSharp/Formats/Tiff/Compression/Compressors/TiffCcittCompressor.cs
  42. 12
      src/ImageSharp/Formats/Tiff/Compression/TiffBaseDecompressor.cs
  43. 5
      src/ImageSharp/Formats/Tiff/Compression/TiffCompressorFactory.cs
  44. 3
      src/ImageSharp/Formats/Tiff/Constants/TiffCompression.cs
  45. 10
      src/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs
  46. 77
      src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs
  47. 68
      src/ImageSharp/Formats/Tiff/Ifd/EntryReader.cs
  48. 2
      src/ImageSharp/Formats/Tiff/README.md
  49. 89
      src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
  50. 20
      src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs
  51. 13
      src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs
  52. 12
      src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs
  53. 21
      src/ImageSharp/Formats/Tiff/TiffFormatType.cs
  54. 41
      src/ImageSharp/Formats/Tiff/TiffImageFormatDetector.cs
  55. 5
      src/ImageSharp/Formats/Tiff/TiffMetadata.cs
  56. 2
      src/ImageSharp/Formats/Tiff/TiffThrowHelper.cs
  57. 2
      src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs
  58. 82
      src/ImageSharp/Formats/Webp/AlphaDecoder.cs
  59. 133
      src/ImageSharp/Formats/Webp/AlphaEncoder.cs
  60. 89
      src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs
  61. 65
      src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs
  62. 24
      src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs
  63. 2
      src/ImageSharp/Formats/Webp/IWebpEncoderOptions.cs
  64. 1
      src/ImageSharp/Formats/Webp/Lossless/LosslessUtils.cs
  65. 42
      src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
  66. 52
      src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
  67. 11
      src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs
  68. 78
      src/ImageSharp/Formats/Webp/WebpDecoderCore.cs
  69. 2
      src/ImageSharp/Formats/Webp/WebpEncoder.cs
  70. 30
      src/ImageSharp/Formats/Webp/WebpEncoderCore.cs
  71. 1
      src/ImageSharp/IO/BufferedReadStream.cs
  72. 1
      src/ImageSharp/IO/ChunkedMemoryStream.cs
  73. 6
      src/ImageSharp/Image.Decode.cs
  74. 10
      src/ImageSharp/ImageSharp.csproj
  75. 56
      src/ImageSharp/Memory/Allocators/Internals/RefCountedLifetimeGuard.cs
  76. 79
      src/ImageSharp/Memory/Allocators/Internals/RefCountedMemoryLifetimeGuard.cs
  77. 2
      src/ImageSharp/Memory/Allocators/Internals/SharedArrayPoolBuffer{T}.cs
  78. 4
      src/ImageSharp/Memory/Allocators/Internals/UniformUnmanagedMemoryPool.LifetimeGuards.cs
  79. 2
      src/ImageSharp/Memory/Allocators/Internals/UnmanagedBufferLifetimeGuard.cs
  80. 4
      src/ImageSharp/Memory/Buffer2DRegion{T}.cs
  81. 2
      src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs
  82. 7
      src/ImageSharp/Metadata/ImageFrameMetadata.cs
  83. 40
      src/ImageSharp/Metadata/ImageMetadata.cs
  84. BIN
      src/ImageSharp/Metadata/Profiles/Exif/DC-X008-Translation-2019-E.pdf
  85. 4
      src/ImageSharp/Metadata/Profiles/Exif/ExifConstants.cs
  86. 17
      src/ImageSharp/Metadata/Profiles/Exif/ExifDataType.cs
  87. 4
      src/ImageSharp/Metadata/Profiles/Exif/ExifDataTypes.cs
  88. 121
      src/ImageSharp/Metadata/Profiles/Exif/ExifEncodedStringHelpers.cs
  89. 298
      src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs
  90. 21
      src/ImageSharp/Metadata/Profiles/Exif/ExifUcs2StringHelpers.cs
  91. 88
      src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs
  92. 25
      src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.ByteArray.cs
  93. 24
      src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.EncodedString.cs
  94. 5
      src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs
  95. 34
      src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Ucs2String.cs
  96. 15
      src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Undefined.cs
  97. 94
      src/ImageSharp/Metadata/Profiles/Exif/Values/EncodedString.cs
  98. 55
      src/ImageSharp/Metadata/Profiles/Exif/Values/ExifEncodedString.cs
  99. 66
      src/ImageSharp/Metadata/Profiles/Exif/Values/ExifLong8.cs
  100. 171
      src/ImageSharp/Metadata/Profiles/Exif/Values/ExifLong8Array.cs

2
.github/CONTRIBUTING.md

@ -28,7 +28,7 @@
#### **Running tests and Debugging** #### **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) * 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?** #### **Do you have questions about consuming the library or the source code?**

33
.github/ISSUE_TEMPLATE/commercial-bug-report.md

@ -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
<!-- A description of the bug or feature -->
### Steps to Reproduce
<!-- List of steps, sample code, failing test or link to a project that reproduces the behavior -->
### System Configuration
<!-- Tell us about the environment where you are experiencing the bug -->
- ImageSharp version:
- Other ImageSharp packages and versions:
- Environment (Operating system, version and so on):
- .NET Framework version:
- Additional information:
<!-- Thanks for reporting the issue to ImageSharp! -->

42
.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.

30
.github/ISSUE_TEMPLATE/oss-bug-report.md

@ -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
<!-- A description of the bug or feature -->
### Steps to Reproduce
<!-- List of steps, sample code, failing test or link to a project that reproduces the behavior -->
### System Configuration
<!-- Tell us about the environment where you are experiencing the bug -->
- ImageSharp version:
- Other ImageSharp packages and versions:
- Environment (Operating system, version and so on):
- .NET Framework version:
- Additional information:
<!-- Thanks for reporting the issue to ImageSharp! -->

40
.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.

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

@ -1,202 +1,198 @@
name: Build name: Build
on: on:
push: push:
branches: branches:
- master - main
tags: tags:
- "v*" - "v*"
pull_request: pull_request:
branches: branches:
- master - main
jobs: jobs:
Build: Build:
strategy: strategy:
matrix: matrix:
options: options:
- os: ubuntu-latest - os: ubuntu-latest
framework: net6.0 framework: net6.0
sdk: 6.0.x sdk: 6.0.x
sdk-preview: true sdk-preview: true
runtime: -x64 runtime: -x64
codecov: false codecov: false
- os: macos-latest - os: macos-latest
framework: net6.0 framework: net6.0
sdk: 6.0.x sdk: 6.0.x
sdk-preview: true sdk-preview: true
runtime: -x64 runtime: -x64
codecov: false codecov: false
- os: windows-latest - os: windows-latest
framework: net6.0 framework: net6.0
sdk: 6.0.x sdk: 6.0.x
sdk-preview: true sdk-preview: true
runtime: -x64 runtime: -x64
codecov: false codecov: false
- os: ubuntu-latest - os: ubuntu-latest
framework: net5.0 framework: net5.0
runtime: -x64 runtime: -x64
codecov: false codecov: false
- os: macos-latest - os: macos-latest
framework: net5.0 framework: net5.0
runtime: -x64 runtime: -x64
codecov: false codecov: false
- os: windows-latest - os: windows-latest
framework: net5.0 framework: net5.0
runtime: -x64 runtime: -x64
codecov: false codecov: false
- os: ubuntu-latest - os: ubuntu-latest
framework: netcoreapp3.1 framework: netcoreapp3.1
runtime: -x64 runtime: -x64
codecov: true codecov: false
- os: macos-latest - os: macos-latest
framework: netcoreapp3.1 framework: netcoreapp3.1
runtime: -x64 runtime: -x64
codecov: false codecov: false
- os: windows-latest - os: windows-latest
framework: netcoreapp3.1 framework: netcoreapp3.1
runtime: -x64 runtime: -x64
codecov: false codecov: false
- os: windows-latest - os: windows-latest
framework: netcoreapp2.1 framework: netcoreapp2.1
runtime: -x64 runtime: -x64
codecov: false codecov: false
- os: windows-latest - os: windows-latest
framework: net472 framework: net472
runtime: -x64 runtime: -x64
codecov: false codecov: false
- os: windows-latest - os: windows-latest
framework: net472 framework: net472
runtime: -x86 runtime: -x86
codecov: false codecov: false
runs-on: ${{matrix.options.os}} runs-on: ${{matrix.options.os}}
steps: steps:
- name: Git Config - name: Git Config
shell: bash shell: bash
run: | run: |
git config --global core.autocrlf false git config --global core.autocrlf false
git config --global core.longpaths true git config --global core.longpaths true
- name: Git Checkout - name: Git Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: recursive submodules: recursive
# See https://github.com/actions/checkout/issues/165#issuecomment-657673315 # See https://github.com/actions/checkout/issues/165#issuecomment-657673315
- name: Git Create LFS FileList - name: Git Create LFS FileList
run: git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id run: git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id
- name: Git Setup LFS Cache - name: Git Setup LFS Cache
uses: actions/cache@v2 uses: actions/cache@v2
id: lfs-cache id: lfs-cache
with: with:
path: .git/lfs path: .git/lfs
key: ${{ runner.os }}-lfs-${{ hashFiles('.lfs-assets-id') }}-v1 key: ${{ runner.os }}-lfs-${{ hashFiles('.lfs-assets-id') }}-v1
- name: Git Pull LFS - name: Git Pull LFS
run: git lfs pull run: git lfs pull
- name: NuGet Install - name: NuGet Install
uses: NuGet/setup-nuget@v1 uses: NuGet/setup-nuget@v1
- name: NuGet Setup Cache - name: NuGet Setup Cache
uses: actions/cache@v2 uses: actions/cache@v2
id: nuget-cache id: nuget-cache
with: with:
path: ~/.nuget path: ~/.nuget
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/*.props', '**/*.targets') }} key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/*.props', '**/*.targets') }}
restore-keys: ${{ runner.os }}-nuget- restore-keys: ${{ runner.os }}-nuget-
- name: DotNet Setup Preview - name: DotNet Setup
if: ${{ matrix.options.sdk-preview == true }} uses: actions/setup-dotnet@v1
uses: actions/setup-dotnet@v1 with:
with: dotnet-version: |
dotnet-version: ${{ matrix.options.sdk }} 6.0.x
include-prerelease: true 5.0.x
3.1.x
- name: DotNet Build 2.1.x
if: ${{ matrix.options.sdk-preview != true }}
shell: pwsh - name: DotNet Build
run: ./ci-build.ps1 "${{matrix.options.framework}}" if: ${{ matrix.options.sdk-preview != true }}
env: shell: pwsh
SIXLABORS_TESTING: True run: ./ci-build.ps1 "${{matrix.options.framework}}"
env:
- name: DotNet Build Preview SIXLABORS_TESTING: True
if: ${{ matrix.options.sdk-preview == true }}
shell: pwsh - name: DotNet Build Preview
run: ./ci-build.ps1 "${{matrix.options.framework}}" if: ${{ matrix.options.sdk-preview == true }}
env: shell: pwsh
SIXLABORS_TESTING_PREVIEW: True run: ./ci-build.ps1 "${{matrix.options.framework}}"
env:
- name: DotNet Test SIXLABORS_TESTING_PREVIEW: True
if: ${{ matrix.options.sdk-preview != true }}
shell: pwsh - name: DotNet Test
run: ./ci-test.ps1 "${{matrix.options.os}}" "${{matrix.options.framework}}" "${{matrix.options.runtime}}" "${{matrix.options.codecov}}" if: ${{ matrix.options.sdk-preview != true }}
env: shell: pwsh
SIXLABORS_TESTING: True run: ./ci-test.ps1 "${{matrix.options.os}}" "${{matrix.options.framework}}" "${{matrix.options.runtime}}" "${{matrix.options.codecov}}"
XUNIT_PATH: .\tests\ImageSharp.Tests # Required for xunit env:
SIXLABORS_TESTING: True
- name: DotNet Test Preview XUNIT_PATH: .\tests\ImageSharp.Tests # Required for xunit
if: ${{ matrix.options.sdk-preview == true }}
shell: pwsh - name: DotNet Test Preview
run: ./ci-test.ps1 "${{matrix.options.os}}" "${{matrix.options.framework}}" "${{matrix.options.runtime}}" "${{matrix.options.codecov}}" if: ${{ matrix.options.sdk-preview == true }}
env: shell: pwsh
SIXLABORS_TESTING_PREVIEW: True run: ./ci-test.ps1 "${{matrix.options.os}}" "${{matrix.options.framework}}" "${{matrix.options.runtime}}" "${{matrix.options.codecov}}"
XUNIT_PATH: .\tests\ImageSharp.Tests # Required for xunit env:
SIXLABORS_TESTING_PREVIEW: True
- name: Export Failed Output XUNIT_PATH: .\tests\ImageSharp.Tests # Required for xunit
uses: actions/upload-artifact@v2
if: failure() - name: Export Failed Output
with: uses: actions/upload-artifact@v2
name: actual_output_${{ runner.os }}_${{ matrix.options.framework }}${{ matrix.options.runtime }}.zip if: failure()
path: tests/Images/ActualOutput/ with:
name: actual_output_${{ runner.os }}_${{ matrix.options.framework }}${{ matrix.options.runtime }}.zip
- name: Codecov Update path: tests/Images/ActualOutput/
uses: codecov/codecov-action@v1
if: matrix.options.codecov == true && startsWith(github.repository, 'SixLabors') Publish:
with: needs: [Build]
flags: unittests
runs-on: ubuntu-latest
Publish:
needs: [Build] if: (github.event_name == 'push')
runs-on: ubuntu-latest steps:
- name: Git Config
if: (github.event_name == 'push') shell: bash
run: |
steps: git config --global core.autocrlf false
- name: Git Config git config --global core.longpaths true
shell: bash
run: | - name: Git Checkout
git config --global core.autocrlf false uses: actions/checkout@v2
git config --global core.longpaths true with:
fetch-depth: 0
- name: Git Checkout submodules: recursive
uses: actions/checkout@v2
with: - name: NuGet Install
fetch-depth: 0 uses: NuGet/setup-nuget@v1
submodules: recursive
- name: NuGet Setup Cache
- name: NuGet Install uses: actions/cache@v2
uses: NuGet/setup-nuget@v1 id: nuget-cache
with:
- name: NuGet Setup Cache path: ~/.nuget
uses: actions/cache@v2 key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/*.props', '**/*.targets') }}
id: nuget-cache restore-keys: ${{ runner.os }}-nuget-
with:
path: ~/.nuget - name: DotNet Pack
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/*.props', '**/*.targets') }} shell: pwsh
restore-keys: ${{ runner.os }}-nuget- run: ./ci-pack.ps1
- name: DotNet Pack - name: MyGet Publish
shell: pwsh shell: pwsh
run: ./ci-pack.ps1 run: |
dotnet nuget push .\artifacts\*.nupkg -k ${{secrets.MYGET_TOKEN}} -s https://www.myget.org/F/sixlabors/api/v2/package
- name: MyGet Publish dotnet nuget push .\artifacts\*.snupkg -k ${{secrets.MYGET_TOKEN}} -s https://www.myget.org/F/sixlabors/api/v3/index.json
shell: pwsh # TODO: If github.ref starts with 'refs/tags' then it was tag push and we can optionally push out package to nuget.org
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

81
.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

1
ImageSharp.sln

@ -551,6 +551,7 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{C0D7754B-5277-438E-ABEB-2BA34401B5A7}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{C0D7754B-5277-438E-ABEB-2BA34401B5A7}"
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
.github\workflows\build-and-test.yml = .github\workflows\build-and-test.yml .github\workflows\build-and-test.yml = .github\workflows\build-and-test.yml
.github\workflows\code-coverage.yml = .github\workflows\code-coverage.yml
EndProjectSection EndProjectSection
EndProject EndProject
Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "SharedInfrastructure", "shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.shproj", "{68A8CC40-6AED-4E96-B524-31B1158FDEEA}" Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "SharedInfrastructure", "shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.shproj", "{68A8CC40-6AED-4E96-B524-31B1158FDEEA}"

12
README.md

@ -1,14 +1,14 @@
<h1 align="center"> <h1 align="center">
<img src="https://github.com/SixLabors/Branding/raw/master/icons/imagesharp/sixlabors.imagesharp.svg?sanitize=true" alt="SixLabors.ImageSharp" width="256"/> <img src="https://github.com/SixLabors/Branding/raw/main/icons/imagesharp/sixlabors.imagesharp.svg?sanitize=true" alt="SixLabors.ImageSharp" width="256"/>
<br/> <br/>
SixLabors.ImageSharp SixLabors.ImageSharp
</h1> </h1>
<div align="center"> <div align="center">
[![Build Status](https://img.shields.io/github/workflow/status/SixLabors/ImageSharp/Build/master)](https://github.com/SixLabors/ImageSharp/actions) [![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/master/graph/badge.svg)](https://codecov.io/gh/SixLabors/ImageSharp) [![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) [![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) [![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 ## Documentation
- [Detailed documentation](https://sixlabors.github.io/docs/) for the ImageSharp API is available. This includes additional conceptual documentation to help you get started. - [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 ## 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. - 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 ## 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. 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? ## 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 ## The ImageSharp Team

98
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
{
/// <summary>
/// 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.
/// </summary>
/// <param name="chars">The hexadecimal string to parse.</param>
/// <param name="bytes">The destination for the parsed bytes. Must be at least <paramref name="chars"/>.Length / 2 bytes long.</param>
/// <returns>The number of bytes written to <paramref name="bytes"/>.</returns>
public static int HexStringToBytes(ReadOnlySpan<char> chars, Span<byte> 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<byte> 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;
}
}
}

12
src/ImageSharp/Configuration.cs

@ -123,25 +123,25 @@ namespace SixLabors.ImageSharp
public ReadOrigin ReadOrigin { get; set; } = ReadOrigin.Current; public ReadOrigin ReadOrigin { get; set; } = ReadOrigin.Current;
/// <summary> /// <summary>
/// Gets or sets the <see cref="ImageFormatManager"/> that is currently in use. /// Gets or the <see cref="ImageFormatManager"/> that is currently in use.
/// </summary> /// </summary>
public ImageFormatManager ImageFormatsManager { get; set; } = new(); public ImageFormatManager ImageFormatsManager { get; private set; } = new ImageFormatManager();
/// <summary> /// <summary>
/// Gets or sets the <see cref="ImageSharp.Memory.MemoryAllocator"/> that is currently in use. /// Gets or sets the <see cref="Memory.MemoryAllocator"/> that is currently in use.
/// Defaults to <see cref="ImageSharp.Memory.MemoryAllocator.Default"/>. /// Defaults to <see cref="MemoryAllocator.Default"/>.
/// <para /> /// <para />
/// Allocators are expensive, so it is strongly recommended to use only one busy instance per process. /// 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 /// In case you need to customize it, you can ensure this by changing
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// It's possible to reduce allocator footprint by assigning a custom instance created with /// It's possible to reduce allocator footprint by assigning a custom instance created with
/// <see cref="Memory.MemoryAllocator.Create(MemoryAllocatorOptions)"/>, but note that since the default pooling /// <see cref="MemoryAllocator.Create(MemoryAllocatorOptions)"/>, but note that since the default pooling
/// allocators are expensive, it is strictly recommended to use a single process-wide allocator. /// allocators are expensive, it is strictly recommended to use a single process-wide allocator.
/// You can ensure this by altering the allocator of <see cref="Default"/>, or by implementing custom application logic that /// You can ensure this by altering the allocator of <see cref="Default"/>, or by implementing custom application logic that
/// manages allocator lifetime. /// manages allocator lifetime.
/// <para /> /// <para />
/// If an allocator has to be dropped for some reason, <see cref="Memory.MemoryAllocator.ReleaseRetainedResources"/> /// If an allocator has to be dropped for some reason, <see cref="MemoryAllocator.ReleaseRetainedResources"/>
/// shall be invoked after disposing all associated <see cref="Image"/> instances. /// shall be invoked after disposing all associated <see cref="Image"/> instances.
/// </remarks> /// </remarks>
public MemoryAllocator MemoryAllocator public MemoryAllocator MemoryAllocator

83
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
{
/// <summary>
/// Represents the method to handle <see cref="MemoryDiagnostics.UndisposedAllocation"/>.
/// </summary>
public delegate void UndisposedAllocationDelegate(string allocationStackTrace);
/// <summary>
/// Utilities to track memory usage and detect memory leaks from not disposing ImageSharp objects.
/// </summary>
public static class MemoryDiagnostics
{
private static int totalUndisposedAllocationCount;
private static UndisposedAllocationDelegate undisposedAllocation;
private static int undisposedAllocationSubscriptionCounter;
private static readonly object SyncRoot = new();
/// <summary>
/// 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 <see cref="TotalUndisposedAllocationCount"/>.
/// </summary>
public static event UndisposedAllocationDelegate UndisposedAllocation
{
add
{
lock (SyncRoot)
{
undisposedAllocationSubscriptionCounter++;
undisposedAllocation += value;
}
}
remove
{
lock (SyncRoot)
{
undisposedAllocation -= value;
undisposedAllocationSubscriptionCounter--;
}
}
}
/// <summary>
/// Gets a value indicating the total number of memory resource objects leaked to the finalizer.
/// </summary>
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
}
}
}

11
src/ImageSharp/Formats/Gif/GifConstants.cs

@ -121,5 +121,16 @@ namespace SixLabors.ImageSharp.Formats.Gif
(byte)'P', (byte)'E', (byte)'P', (byte)'E',
(byte)'2', (byte)'.', (byte)'0' (byte)'2', (byte)'.', (byte)'0'
}; };
/// <summary>
/// Gets the ASCII encoded application identification bytes.
/// </summary>
internal static ReadOnlySpan<byte> XmpApplicationIdentificationBytes => new[]
{
(byte)'X', (byte)'M', (byte)'P',
(byte)' ', (byte)'D', (byte)'a',
(byte)'t', (byte)'a',
(byte)'X', (byte)'M', (byte)'P'
};
} }
} }

78
src/ImageSharp/Formats/Gif/GifDecoderCore.cs

@ -11,6 +11,7 @@ using System.Threading;
using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Gif namespace SixLabors.ImageSharp.Formats.Gif
@ -250,7 +251,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
} }
/// <summary> /// <summary>
/// Reads the application extension block parsing any animation information /// Reads the application extension block parsing any animation or XMP information
/// if present. /// if present.
/// </summary> /// </summary>
private void ReadApplicationExtension() private void ReadApplicationExtension()
@ -258,25 +259,37 @@ namespace SixLabors.ImageSharp.Formats.Gif
int appLength = this.stream.ReadByte(); int appLength = this.stream.ReadByte();
// If the length is 11 then it's a valid extension and most likely // 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) if (appLength == GifConstants.ApplicationBlockSize)
{ {
this.stream.Skip(appLength); this.stream.Read(this.buffer, 0, GifConstants.ApplicationBlockSize);
int subBlockSize = this.stream.ReadByte(); bool isXmp = this.buffer.AsSpan().StartsWith(GifConstants.XmpApplicationIdentificationBytes);
// TODO: There's also a NETSCAPE buffer extension. if (isXmp)
// http://www.vurdalakov.net/misc/gif/netscape-buffering-application-extension
if (subBlockSize == GifConstants.NetscapeLoopingSubBlockSize)
{ {
this.stream.Read(this.buffer, 0, GifConstants.NetscapeLoopingSubBlockSize); var extension = GifXmpApplicationExtension.Read(this.stream);
this.gifMetadata.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.AsSpan(1)).RepeatCount; this.metadata.XmpProfile = new XmpProfile(extension.Data);
this.stream.Skip(1); // Skip the terminator.
return; 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; return;
} }
@ -363,7 +376,17 @@ namespace SixLabors.ImageSharp.Formats.Gif
indices = this.Configuration.MemoryAllocator.Allocate2D<byte>(this.imageDescriptor.Width, this.imageDescriptor.Height, AllocationOptions.Clean); indices = this.Configuration.MemoryAllocator.Allocate2D<byte>(this.imageDescriptor.Width, this.imageDescriptor.Height, AllocationOptions.Clean);
this.ReadFrameIndices(indices); this.ReadFrameIndices(indices);
ReadOnlySpan<Rgb24> colorTable = MemoryMarshal.Cast<byte, Rgb24>((localColorTable ?? this.globalColorTable).GetSpan()); Span<byte> rawColorTable = default;
if (localColorTable != null)
{
rawColorTable = localColorTable.GetSpan();
}
else if (this.globalColorTable != null)
{
rawColorTable = this.globalColorTable.GetSpan();
}
ReadOnlySpan<Rgb24> colorTable = MemoryMarshal.Cast<byte, Rgb24>(rawColorTable);
this.ReadFrameColors(ref image, ref previousFrame, indices, colorTable, this.imageDescriptor); this.ReadFrameColors(ref image, ref previousFrame, indices, colorTable, this.imageDescriptor);
// Skip any remaining blocks // Skip any remaining blocks
@ -402,6 +425,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
{ {
int imageWidth = this.logicalScreenDescriptor.Width; int imageWidth = this.logicalScreenDescriptor.Width;
int imageHeight = this.logicalScreenDescriptor.Height; int imageHeight = this.logicalScreenDescriptor.Height;
bool transFlag = this.graphicsControlExtension.TransparencyFlag;
ImageFrame<TPixel> prevFrame = null; ImageFrame<TPixel> prevFrame = null;
ImageFrame<TPixel> currentFrame = null; ImageFrame<TPixel> currentFrame = null;
@ -409,8 +433,15 @@ namespace SixLabors.ImageSharp.Formats.Gif
if (previousFrame is null) if (previousFrame is null)
{ {
// This initializes the image to become fully transparent because the alpha channel is zero. if (!transFlag)
image = new Image<TPixel>(this.Configuration, imageWidth, imageHeight, this.metadata); {
image = new Image<TPixel>(this.Configuration, imageWidth, imageHeight, Color.Black.ToPixel<TPixel>(), this.metadata);
}
else
{
// This initializes the image to become fully transparent because the alpha channel is zero.
image = new Image<TPixel>(this.Configuration, imageWidth, imageHeight, this.metadata);
}
this.SetFrameMetadata(image.Frames.RootFrame.Metadata); this.SetFrameMetadata(image.Frames.RootFrame.Metadata);
@ -432,6 +463,11 @@ namespace SixLabors.ImageSharp.Formats.Gif
this.RestoreToBackground(imageFrame); this.RestoreToBackground(imageFrame);
} }
if (colorTable.Length == 0)
{
return;
}
int interlacePass = 0; // The interlace pass int interlacePass = 0; // The interlace pass
int interlaceIncrement = 8; // The interlacing line increment int interlaceIncrement = 8; // The interlacing line increment
int interlaceY = 0; // The current interlaced line int interlaceY = 0; // The current interlaced line
@ -439,7 +475,6 @@ namespace SixLabors.ImageSharp.Formats.Gif
int descriptorBottom = descriptorTop + descriptor.Height; int descriptorBottom = descriptorTop + descriptor.Height;
int descriptorLeft = descriptor.Left; int descriptorLeft = descriptor.Left;
int descriptorRight = descriptorLeft + descriptor.Width; int descriptorRight = descriptorLeft + descriptor.Width;
bool transFlag = this.graphicsControlExtension.TransparencyFlag;
byte transIndex = this.graphicsControlExtension.TransparencyIndex; byte transIndex = this.graphicsControlExtension.TransparencyIndex;
int colorTableMaxIdx = colorTable.Length - 1; int colorTableMaxIdx = colorTable.Length - 1;
@ -622,10 +657,13 @@ namespace SixLabors.ImageSharp.Formats.Gif
int globalColorTableLength = this.logicalScreenDescriptor.GlobalColorTableSize * 3; int globalColorTableLength = this.logicalScreenDescriptor.GlobalColorTableSize * 3;
this.gifMetadata.GlobalColorTableLength = globalColorTableLength; this.gifMetadata.GlobalColorTableLength = globalColorTableLength;
this.globalColorTable = this.MemoryAllocator.Allocate<byte>(globalColorTableLength, AllocationOptions.Clean); if (globalColorTableLength > 0)
{
this.globalColorTable = this.MemoryAllocator.Allocate<byte>(globalColorTableLength, AllocationOptions.Clean);
// Read the global color table data from the stream // Read the global color table data from the stream
stream.Read(this.globalColorTable.GetSpan()); stream.Read(this.globalColorTable.GetSpan());
}
} }
} }
} }

47
src/ImageSharp/Formats/Gif/GifEncoderCore.cs

@ -10,6 +10,7 @@ using System.Threading;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization; using SixLabors.ImageSharp.Processing.Processors.Quantization;
@ -121,11 +122,8 @@ namespace SixLabors.ImageSharp.Formats.Gif
// Write the comments. // Write the comments.
this.WriteComments(gifMetadata, stream); this.WriteComments(gifMetadata, stream);
// Write application extension to allow additional frames. // Write application extensions.
if (image.Frames.Count > 1) this.WriteApplicationExtensions(stream, image.Frames.Count, gifMetadata.RepeatCount, metadata.XmpProfile);
{
this.WriteApplicationExtension(stream, gifMetadata.RepeatCount);
}
if (useGlobalTable) if (useGlobalTable)
{ {
@ -326,15 +324,24 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// Writes the application extension to the stream. /// Writes the application extension to the stream.
/// </summary> /// </summary>
/// <param name="stream">The stream to write to.</param> /// <param name="stream">The stream to write to.</param>
/// <param name="frameCount">The frame count fo this image.</param>
/// <param name="repeatCount">The animated image repeat count.</param> /// <param name="repeatCount">The animated image repeat count.</param>
private void WriteApplicationExtension(Stream stream, ushort repeatCount) /// <param name="xmpProfile">The XMP metadata profile. Null if profile is not to be written.</param>
private void WriteApplicationExtensions(Stream stream, int frameCount, ushort repeatCount, XmpProfile xmpProfile)
{ {
// Application Extension Header // Application Extension: Loop repeat count.
if (repeatCount != 1) if (frameCount > 1 && repeatCount != 1)
{ {
var loopingExtension = new GifNetscapeLoopingApplicationExtension(repeatCount); var loopingExtension = new GifNetscapeLoopingApplicationExtension(repeatCount);
this.WriteExtension(loopingExtension, stream); this.WriteExtension(loopingExtension, stream);
} }
// Application Extension: XMP Profile.
if (xmpProfile != null)
{
var xmpExtension = new GifXmpApplicationExtension(xmpProfile.Data);
this.WriteExtension(xmpExtension, stream);
}
} }
/// <summary> /// <summary>
@ -420,14 +427,28 @@ namespace SixLabors.ImageSharp.Formats.Gif
private void WriteExtension<TGifExtension>(TGifExtension extension, Stream stream) private void WriteExtension<TGifExtension>(TGifExtension extension, Stream stream)
where TGifExtension : struct, IGifExtension where TGifExtension : struct, IGifExtension
{ {
this.buffer[0] = GifConstants.ExtensionIntroducer; IMemoryOwner<byte> owner = null;
this.buffer[1] = extension.Label; Span<byte> buffer;
int extensionSize = extension.ContentLength;
if (extensionSize > this.buffer.Length - 3)
{
owner = this.memoryAllocator.Allocate<byte>(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();
} }
/// <summary> /// <summary>

4
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. // Licensed under the Apache License, Version 2.0.
using System; using System;
@ -63,6 +63,8 @@ namespace SixLabors.ImageSharp.Formats.Gif
byte IGifExtension.Label => GifConstants.GraphicControlLabel; byte IGifExtension.Label => GifConstants.GraphicControlLabel;
int IGifExtension.ContentLength => 5;
public int WriteTo(Span<byte> buffer) public int WriteTo(Span<byte> buffer)
{ {
ref GifGraphicControlExtension dest = ref Unsafe.As<byte, GifGraphicControlExtension>(ref MemoryMarshal.GetReference(buffer)); ref GifGraphicControlExtension dest = ref Unsafe.As<byte, GifGraphicControlExtension>(ref MemoryMarshal.GetReference(buffer));

2
src/ImageSharp/Formats/Gif/Sections/GifNetscapeLoopingApplicationExtension.cs

@ -12,6 +12,8 @@ namespace SixLabors.ImageSharp.Formats.Gif
public byte Label => GifConstants.ApplicationExtensionLabel; public byte Label => GifConstants.ApplicationExtensionLabel;
public int ContentLength => 16;
/// <summary> /// <summary>
/// Gets the repeat count. /// Gets the repeat count.
/// 0 means loop indefinitely. Count is set as play n + 1 times. /// 0 means loop indefinitely. Count is set as play n + 1 times.

97
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
/// <summary>
/// Gets the raw Data.
/// </summary>
public byte[] Data { get; }
/// <summary>
/// Reads the XMP metadata from the specified stream.
/// </summary>
/// <param name="stream">The stream to read from.</param>
/// <returns>The XMP metadata</returns>
/// <exception cref="ImageFormatException">Thrown if the XMP block is not properly terminated.</exception>
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<byte[]>();
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<byte> 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<byte> 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<byte> 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<byte> 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;
}
}
}

7
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. // Licensed under the Apache License, Version 2.0.
using System; using System;
@ -15,6 +15,11 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary> /// </summary>
byte Label { get; } byte Label { get; }
/// <summary>
/// Gets the length of the contents of this extension.
/// </summary>
int ContentLength { get; }
/// <summary> /// <summary>
/// Writes the extension data to the buffer. /// Writes the extension data to the buffer.
/// </summary> /// </summary>

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

@ -106,7 +106,6 @@ namespace SixLabors.ImageSharp
/// <param name="stream">The stream to save the image to.</param> /// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param> /// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception> /// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static void SaveAsBmp(this Image source, Stream stream, BmpEncoder encoder) public static void SaveAsBmp(this Image source, Stream stream, BmpEncoder encoder)
=> source.Save( => source.Save(
stream, stream,
@ -209,7 +208,6 @@ namespace SixLabors.ImageSharp
/// <param name="stream">The stream to save the image to.</param> /// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param> /// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception> /// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static void SaveAsGif(this Image source, Stream stream, GifEncoder encoder) public static void SaveAsGif(this Image source, Stream stream, GifEncoder encoder)
=> source.Save( => source.Save(
stream, stream,
@ -312,7 +310,6 @@ namespace SixLabors.ImageSharp
/// <param name="stream">The stream to save the image to.</param> /// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param> /// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception> /// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static void SaveAsJpeg(this Image source, Stream stream, JpegEncoder encoder) public static void SaveAsJpeg(this Image source, Stream stream, JpegEncoder encoder)
=> source.Save( => source.Save(
stream, stream,
@ -415,7 +412,6 @@ namespace SixLabors.ImageSharp
/// <param name="stream">The stream to save the image to.</param> /// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param> /// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception> /// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static void SaveAsPbm(this Image source, Stream stream, PbmEncoder encoder) public static void SaveAsPbm(this Image source, Stream stream, PbmEncoder encoder)
=> source.Save( => source.Save(
stream, stream,
@ -518,7 +514,6 @@ namespace SixLabors.ImageSharp
/// <param name="stream">The stream to save the image to.</param> /// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param> /// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception> /// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static void SaveAsPng(this Image source, Stream stream, PngEncoder encoder) public static void SaveAsPng(this Image source, Stream stream, PngEncoder encoder)
=> source.Save( => source.Save(
stream, stream,
@ -621,7 +616,6 @@ namespace SixLabors.ImageSharp
/// <param name="stream">The stream to save the image to.</param> /// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param> /// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception> /// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static void SaveAsTga(this Image source, Stream stream, TgaEncoder encoder) public static void SaveAsTga(this Image source, Stream stream, TgaEncoder encoder)
=> source.Save( => source.Save(
stream, stream,
@ -724,7 +718,6 @@ namespace SixLabors.ImageSharp
/// <param name="stream">The stream to save the image to.</param> /// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param> /// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception> /// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static void SaveAsWebp(this Image source, Stream stream, WebpEncoder encoder) public static void SaveAsWebp(this Image source, Stream stream, WebpEncoder encoder)
=> source.Save( => source.Save(
stream, stream,
@ -827,7 +820,6 @@ namespace SixLabors.ImageSharp
/// <param name="stream">The stream to save the image to.</param> /// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param> /// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception> /// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static void SaveAsTiff(this Image source, Stream stream, TiffEncoder encoder) public static void SaveAsTiff(this Image source, Stream stream, TiffEncoder encoder)
=> source.Save( => source.Save(
stream, stream,

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

@ -124,7 +124,6 @@ namespace SixLabors.ImageSharp
/// <param name="stream">The stream to save the image to.</param> /// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param> /// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception> /// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static void SaveAs<#= fmt #>(this Image source, Stream stream, <#= fmt #>Encoder encoder) public static void SaveAs<#= fmt #>(this Image source, Stream stream, <#= fmt #>Encoder encoder)
=> source.Save( => source.Save(
stream, stream,

26
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. // Licensed under the Apache License, Version 2.0.
using System; using System;
@ -17,27 +17,27 @@ namespace SixLabors.ImageSharp.Formats
/// Used for locking against as there is no ConcurrentSet type. /// Used for locking against as there is no ConcurrentSet type.
/// <see href="https://github.com/dotnet/corefx/issues/6318"/> /// <see href="https://github.com/dotnet/corefx/issues/6318"/>
/// </summary> /// </summary>
private static readonly object HashLock = new object(); private static readonly object HashLock = new();
/// <summary> /// <summary>
/// The list of supported <see cref="IImageEncoder"/> keyed to mime types. /// The list of supported <see cref="IImageEncoder"/> keyed to mime types.
/// </summary> /// </summary>
private readonly ConcurrentDictionary<IImageFormat, IImageEncoder> mimeTypeEncoders = new ConcurrentDictionary<IImageFormat, IImageEncoder>(); private readonly ConcurrentDictionary<IImageFormat, IImageEncoder> mimeTypeEncoders = new();
/// <summary> /// <summary>
/// The list of supported <see cref="IImageEncoder"/> keyed to mime types. /// The list of supported <see cref="IImageEncoder"/> keyed to mime types.
/// </summary> /// </summary>
private readonly ConcurrentDictionary<IImageFormat, IImageDecoder> mimeTypeDecoders = new ConcurrentDictionary<IImageFormat, IImageDecoder>(); private readonly ConcurrentDictionary<IImageFormat, IImageDecoder> mimeTypeDecoders = new();
/// <summary> /// <summary>
/// The list of supported <see cref="IImageFormat"/>s. /// The list of supported <see cref="IImageFormat"/>s.
/// </summary> /// </summary>
private readonly HashSet<IImageFormat> imageFormats = new HashSet<IImageFormat>(); private readonly HashSet<IImageFormat> imageFormats = new();
/// <summary> /// <summary>
/// The list of supported <see cref="IImageFormatDetector"/>s. /// The list of supported <see cref="IImageFormatDetector"/>s.
/// </summary> /// </summary>
private ConcurrentBag<IImageFormatDetector> imageFormatDetectors = new ConcurrentBag<IImageFormatDetector>(); private ConcurrentBag<IImageFormatDetector> imageFormatDetectors = new();
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ImageFormatManager" /> class. /// Initializes a new instance of the <see cref="ImageFormatManager" /> class.
@ -113,9 +113,7 @@ namespace SixLabors.ImageSharp.Formats
/// <param name="mimeType">The mime-type to discover</param> /// <param name="mimeType">The mime-type to discover</param>
/// <returns>The <see cref="IImageFormat"/> if found; otherwise null</returns> /// <returns>The <see cref="IImageFormat"/> if found; otherwise null</returns>
public IImageFormat FindFormatByMimeType(string mimeType) public IImageFormat FindFormatByMimeType(string mimeType)
{ => this.imageFormats.FirstOrDefault(x => x.MimeTypes.Contains(mimeType, StringComparer.OrdinalIgnoreCase));
return this.imageFormats.FirstOrDefault(x => x.MimeTypes.Contains(mimeType, StringComparer.OrdinalIgnoreCase));
}
/// <summary> /// <summary>
/// Sets a specific image encoder as the encoder for a specific image format. /// Sets a specific image encoder as the encoder for a specific image format.
@ -146,10 +144,7 @@ namespace SixLabors.ImageSharp.Formats
/// <summary> /// <summary>
/// Removes all the registered image format detectors. /// Removes all the registered image format detectors.
/// </summary> /// </summary>
public void ClearImageFormatDetectors() public void ClearImageFormatDetectors() => this.imageFormatDetectors = new();
{
this.imageFormatDetectors = new ConcurrentBag<IImageFormatDetector>();
}
/// <summary> /// <summary>
/// Adds a new detector for detecting mime types. /// Adds a new detector for detecting mime types.
@ -193,9 +188,6 @@ namespace SixLabors.ImageSharp.Formats
/// <summary> /// <summary>
/// Sets the max header size. /// Sets the max header size.
/// </summary> /// </summary>
private void SetMaxHeaderSize() private void SetMaxHeaderSize() => this.MaxHeaderSize = this.imageFormatDetectors.Max(x => x.HeaderSize);
{
this.MaxHeaderSize = this.imageFormatDetectors.Max(x => x.HeaderSize);
}
} }
} }

8
src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs

@ -280,7 +280,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
} }
/// <summary> /// <summary>
/// Quantize input block, apply zig-zag ordering and store result as 16bit integers. /// Quantize input block, transpose, apply zig-zag ordering and store as <see cref="Block8x8"/>.
/// </summary> /// </summary>
/// <param name="block">Source block.</param> /// <param name="block">Source block.</param>
/// <param name="dest">Destination block.</param> /// <param name="dest">Destination block.</param>
@ -291,19 +291,19 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
if (Avx2.IsSupported) if (Avx2.IsSupported)
{ {
MultiplyIntoInt16_Avx2(ref block, ref qt, ref dest); MultiplyIntoInt16_Avx2(ref block, ref qt, ref dest);
ZigZag.ApplyZigZagOrderingAvx2(ref dest); ZigZag.ApplyTransposingZigZagOrderingAvx2(ref dest);
} }
else if (Ssse3.IsSupported) else if (Ssse3.IsSupported)
{ {
MultiplyIntoInt16_Sse2(ref block, ref qt, ref dest); MultiplyIntoInt16_Sse2(ref block, ref qt, ref dest);
ZigZag.ApplyZigZagOrderingSsse3(ref dest); ZigZag.ApplyTransposingZigZagOrderingSsse3(ref dest);
} }
else else
#endif #endif
{ {
for (int i = 0; i < Size; i++) for (int i = 0; i < Size; i++)
{ {
int idx = ZigZag.ZigZagOrder[i]; int idx = ZigZag.TransposingOrder[i];
float quantizedVal = block[idx] * qt[idx]; float quantizedVal = block[idx] * qt[idx];
quantizedVal += quantizedVal < 0 ? -0.5f : 0.5f; quantizedVal += quantizedVal < 0 ? -0.5f : 0.5f;
dest[i] = (short)quantizedVal; dest[i] = (short)quantizedVal;

103
src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs

@ -31,7 +31,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// <summary> /// <summary>
/// Number of component in the current scan. /// Number of component in the current scan.
/// </summary> /// </summary>
private int componentsCount; private int scanComponentCount;
/// <summary> /// <summary>
/// The reset interval determined by RST markers. /// The reset interval determined by RST markers.
@ -112,11 +112,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// <summary> /// <summary>
/// Decodes the entropy coded data. /// Decodes the entropy coded data.
/// </summary> /// </summary>
public void ParseEntropyCodedData(int componentCount) /// <param name="scanComponentCount">Component count in the current scan.</param>
public void ParseEntropyCodedData(int scanComponentCount)
{ {
this.cancellationToken.ThrowIfCancellationRequested(); this.cancellationToken.ThrowIfCancellationRequested();
this.componentsCount = componentCount; this.scanComponentCount = scanComponentCount;
this.scanBuffer = new HuffmanScanBuffer(this.stream); this.scanBuffer = new HuffmanScanBuffer(this.stream);
@ -148,11 +149,16 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
private void ParseBaselineData() private void ParseBaselineData()
{ {
if (this.componentsCount == this.frame.ComponentCount) if (this.scanComponentCount != 1)
{ {
this.ParseBaselineDataInterleaved(); this.ParseBaselineDataInterleaved();
this.spectralConverter.CommitConversion(); this.spectralConverter.CommitConversion();
} }
else if (this.frame.ComponentCount == 1)
{
this.ParseBaselineDataSingleComponent();
this.spectralConverter.CommitConversion();
}
else else
{ {
this.ParseBaselineDataNonInterleaved(); this.ParseBaselineDataNonInterleaved();
@ -161,24 +167,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
private void ParseBaselineDataInterleaved() private void ParseBaselineDataInterleaved()
{ {
// Interleaved
int mcu = 0; int mcu = 0;
int mcusPerColumn = this.frame.McusPerColumn; int mcusPerColumn = this.frame.McusPerColumn;
int mcusPerLine = this.frame.McusPerLine; int mcusPerLine = this.frame.McusPerLine;
ref HuffmanScanBuffer buffer = ref this.scanBuffer; 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++) for (int j = 0; j < mcusPerColumn; j++)
{ {
this.cancellationToken.ThrowIfCancellationRequested(); this.cancellationToken.ThrowIfCancellationRequested();
@ -188,7 +181,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
{ {
// Scan an interleaved mcu... process components in order // Scan an interleaved mcu... process components in order
int mcuCol = 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]; int order = this.frame.ComponentOrder[k];
JpegComponent component = this.components[order]; JpegComponent component = this.components[order];
@ -210,7 +203,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
{ {
if (buffer.NoData) 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?) // so we need to decode what's left and return (or maybe throw?)
this.spectralConverter.ConvertStrideBaseline(); this.spectralConverter.ConvertStrideBaseline();
return; return;
@ -233,7 +226,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
this.HandleRestart(); this.HandleRestart();
} }
// convert from spectral to actual pixels via given converter // Convert from spectral to actual pixels via given converter
this.spectralConverter.ConvertStrideBaseline(); this.spectralConverter.ConvertStrideBaseline();
} }
} }
@ -248,8 +241,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DCHuffmanTableId]; ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DCHuffmanTableId];
ref HuffmanTable acHuffmanTable = ref this.acHuffmanTables[component.ACHuffmanTableId]; ref HuffmanTable acHuffmanTable = ref this.acHuffmanTables[component.ACHuffmanTableId];
dcHuffmanTable.Configure();
acHuffmanTable.Configure();
for (int j = 0; j < h; j++) 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<Block8x8> 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() private void CheckProgressiveData()
{ {
// Validate successive scan parameters. // Validate successive scan parameters.
@ -297,7 +334,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
} }
// AC scans may have only one component. // AC scans may have only one component.
if (this.componentsCount != 1) if (this.scanComponentCount != 1)
{ {
invalid = true; invalid = true;
} }
@ -329,7 +366,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
{ {
this.CheckProgressiveData(); this.CheckProgressiveData();
if (this.componentsCount == 1) if (this.scanComponentCount == 1)
{ {
this.ParseProgressiveDataNonInterleaved(); this.ParseProgressiveDataNonInterleaved();
} }
@ -347,15 +384,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
int mcusPerLine = this.frame.McusPerLine; int mcusPerLine = this.frame.McusPerLine;
ref HuffmanScanBuffer buffer = ref this.scanBuffer; 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 j = 0; j < mcusPerColumn; j++)
{ {
for (int i = 0; i < mcusPerLine; i++) 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 // Scan an interleaved mcu... process components in order
int mcuRow = mcu / mcusPerLine; int mcuRow = mcu / mcusPerLine;
int mcuCol = 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]; int order = this.frame.ComponentOrder[k];
JpegComponent component = this.components[order]; JpegComponent component = this.components[order];
@ -416,7 +444,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
if (this.SpectralStart == 0) if (this.SpectralStart == 0)
{ {
ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DCHuffmanTableId]; ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DCHuffmanTableId];
dcHuffmanTable.Configure();
for (int j = 0; j < h; j++) for (int j = 0; j < h; j++)
{ {
@ -444,7 +471,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
else else
{ {
ref HuffmanTable acHuffmanTable = ref this.acHuffmanTables[component.ACHuffmanTableId]; ref HuffmanTable acHuffmanTable = ref this.acHuffmanTables[component.ACHuffmanTableId];
acHuffmanTable.Configure();
for (int j = 0; j < h; j++) for (int j = 0; j < h; j++)
{ {
@ -752,11 +778,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// <param name="index">Table index.</param> /// <param name="index">Table index.</param>
/// <param name="codeLengths">Code lengths.</param> /// <param name="codeLengths">Code lengths.</param>
/// <param name="values">Code values.</param> /// <param name="values">Code values.</param>
/// <param name="workspace">The provided spare workspace memory, can be dirty.</param>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public void BuildHuffmanTable(int type, int index, ReadOnlySpan<byte> codeLengths, ReadOnlySpan<byte> values) public void BuildHuffmanTable(int type, int index, ReadOnlySpan<byte> codeLengths, ReadOnlySpan<byte> values, Span<uint> workspace)
{ {
HuffmanTable[] tables = type == 0 ? this.dcHuffmanTables : this.acHuffmanTables; HuffmanTable[] tables = type == 0 ? this.dcHuffmanTables : this.acHuffmanTables;
tables[index] = new HuffmanTable(codeLengths, values); tables[index] = new HuffmanTable(codeLengths, values, workspace);
} }
} }
} }

76
src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs

@ -13,12 +13,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
[StructLayout(LayoutKind.Sequential)] [StructLayout(LayoutKind.Sequential)]
internal unsafe struct HuffmanTable internal unsafe struct HuffmanTable
{ {
private bool isConfigured;
/// <summary> /// <summary>
/// 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 <see cref="HuffmanTable"/> ctor.
/// </summary> /// </summary>
public fixed byte Sizes[17]; public const int WorkspaceByteSize = 256 * sizeof(uint);
/// <summary> /// <summary>
/// Derived from the DHT marker. Contains the symbols, in order of incremental code length. /// 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
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="HuffmanTable"/> struct. /// Initializes a new instance of the <see cref="HuffmanTable"/> struct.
/// </summary> /// </summary>
/// <param name="codeLengths">The code lengths</param> /// <param name="codeLengths">The code lengths.</param>
/// <param name="values">The huffman values</param> /// <param name="values">The huffman values.</param>
public HuffmanTable(ReadOnlySpan<byte> codeLengths, ReadOnlySpan<byte> values) /// <param name="workspace">The provided spare workspace memory, can be dirty.</param>
public HuffmanTable(ReadOnlySpan<byte> codeLengths, ReadOnlySpan<byte> values, Span<uint> 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); Unsafe.CopyBlockUnaligned(ref this.Values[0], ref MemoryMarshal.GetReference(values), (uint)values.Length);
}
/// <summary>
/// Expands the HuffmanTable into its readable form.
/// </summary>
public void Configure()
{
if (this.isConfigured)
{
return;
}
Span<char> huffSize = stackalloc char[257]; // Generate codes
Span<uint> huffCode = stackalloc uint[257]; uint code = 0;
int si = 1;
// Figure C.1: make table of Huffman code length for each symbol
int p = 0; int p = 0;
for (int j = 1; j <= 16; j++) for (int i = 1; i <= 16; i++)
{ {
int i = this.Sizes[j]; int count = codeLengths[i];
while (i-- != 0) for (int j = 0; j < count; j++)
{ {
huffSize[p++] = (char)j; workspace[p++] = code;
code++;
} }
}
huffSize[p] = (char)0; // 'code' is now 1 more than the last code used for codelength 'si'
// in the valid worst possible case 'code' would have the least
// Figure C.2: generate the codes themselves // significant bit set to 1, e.g. 1111(0) +1 => 1111(1)
uint code = 0; // but it must still fit in 'si' bits since no huffman code can be equal to all 1s
int si = huffSize[0]; // if last code is all ones, e.g. 1111(1), then incrementing it by 1 would yield
p = 0; // a new code which occupies one extra bit, e.g. 1111(1) +1 => (1)1111(0)
while (huffSize[p] != 0) if (code >= (1 << si))
{
while (huffSize[p] == si)
{ {
huffCode[p++] = code; JpegThrowHelper.ThrowInvalidImageContentException("Bad huffman table.");
code++;
} }
code <<= 1; code <<= 1;
@ -113,11 +95,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
p = 0; p = 0;
for (int j = 1; j <= 16; j++) for (int j = 1; j <= 16; j++)
{ {
if (this.Sizes[j] != 0) if (codeLengths[j] != 0)
{ {
this.ValOffset[j] = p - (int)huffCode[p]; this.ValOffset[j] = p - (int)workspace[p];
p += this.Sizes[j]; p += codeLengths[j];
this.MaxCode[j] = huffCode[p - 1]; // Maximum code of length l this.MaxCode[j] = workspace[p - 1]; // Maximum code of length l
this.MaxCode[j] <<= JpegConstants.Huffman.RegisterSize - j; // Left justify this.MaxCode[j] <<= JpegConstants.Huffman.RegisterSize - j; // Left justify
this.MaxCode[j] |= (1ul << (JpegConstants.Huffman.RegisterSize - j)) - 1; 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++) for (int length = 1; length <= JpegConstants.Huffman.LookupBits; length++)
{ {
int jShift = 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[]. // length = current code's length, p = its index in huffCode[] & Values[].
// Generate left-justified code followed by all possible bit sequences // 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--) for (int ctr = 1 << (JpegConstants.Huffman.LookupBits - length); ctr > 0; ctr--)
{ {
this.LookaheadSize[lookBits] = (byte)length; this.LookaheadSize[lookBits] = (byte)length;
@ -155,8 +137,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
} }
} }
} }
this.isConfigured = true;
} }
} }
} }

10
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. // Licensed under the Apache License, Version 2.0.
using System; using System;
@ -103,26 +103,22 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
/// <inheritdoc/> /// <inheritdoc/>
public bool Equals(JFifMarker other) public bool Equals(JFifMarker other)
{ => this.MajorVersion == other.MajorVersion
return this.MajorVersion == other.MajorVersion
&& this.MinorVersion == other.MinorVersion && this.MinorVersion == other.MinorVersion
&& this.DensityUnits == other.DensityUnits && this.DensityUnits == other.DensityUnits
&& this.XDensity == other.XDensity && this.XDensity == other.XDensity
&& this.YDensity == other.YDensity; && this.YDensity == other.YDensity;
}
/// <inheritdoc/> /// <inheritdoc/>
public override bool Equals(object obj) => obj is JFifMarker other && this.Equals(other); public override bool Equals(object obj) => obj is JFifMarker other && this.Equals(other);
/// <inheritdoc/> /// <inheritdoc/>
public override int GetHashCode() public override int GetHashCode()
{ => HashCode.Combine(
return HashCode.Combine(
this.MajorVersion, this.MajorVersion,
this.MinorVersion, this.MinorVersion,
this.DensityUnits, this.DensityUnits,
this.XDensity, this.XDensity,
this.YDensity); this.YDensity);
}
} }
} }

12
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' (byte)'E', (byte)'x', (byte)'i', (byte)'f', (byte)'\0', (byte)'\0'
}; };
/// <summary>
/// Gets the XMP specific markers.
/// </summary>
public static ReadOnlySpan<byte> 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
};
/// <summary> /// <summary>
/// Gets the Adobe specific markers <see href="http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/JPEG.html#Adobe"/>. /// Gets the Adobe specific markers <see href="http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/JPEG.html#Adobe"/>.
/// </summary> /// </summary>

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

@ -111,7 +111,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
this.pixelRowsPerStep = majorVerticalSamplingFactor * blockPixelHeight; this.pixelRowsPerStep = majorVerticalSamplingFactor * blockPixelHeight;
// pixel buffer for resulting image // pixel buffer for resulting image
this.pixelBuffer = allocator.Allocate2D<TPixel>(frame.PixelWidth, frame.PixelHeight); this.pixelBuffer = allocator.Allocate2D<TPixel>(
frame.PixelWidth,
frame.PixelHeight,
this.configuration.PreferContiguousImageBuffers);
this.paddedProxyPixelRow = allocator.Allocate<TPixel>(frame.PixelWidth + 3); this.paddedProxyPixelRow = allocator.Allocate<TPixel>(frame.PixelWidth + 3);
// component processors from spectral to Rgba32 // component processors from spectral to Rgba32

5
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."); DebugGuard.IsTrue(Avx.IsSupported, "Avx support is required to execute this operation.");
// First pass - process rows // First pass - process columns
block.TransposeInplace();
FDCT8x8_1D_Avx(ref block); FDCT8x8_1D_Avx(ref block);
// Second pass - process columns // Second pass - process rows
block.TransposeInplace(); block.TransposeInplace();
FDCT8x8_1D_Avx(ref block); FDCT8x8_1D_Avx(ref block);

139
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 = 0.125f / (tableRef * Unsafe.Add(ref multipliersRef, i));
tableRef = ref Unsafe.Add(ref tableRef, 1); 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();
} }
/// <summary> /// <summary>
@ -133,24 +138,21 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
} }
else else
#endif #endif
if (Vector.IsHardwareAccelerated)
{ {
FDCT_Vector4(ref block); FDCT_Vector4(ref block);
} }
else
{
FDCT_Scalar(ref block);
}
} }
/// <summary> /// <summary>
/// Apply floating point IDCT inplace using <see cref="Vector4"/> API. /// Apply floating point IDCT inplace using <see cref="Vector4"/> API.
/// </summary> /// </summary>
/// <remarks>
/// This method can be used even if there's no SIMD intrinsics available
/// as <see cref="Vector4"/> can be compiled to scalar instructions.
/// </remarks>
/// <param name="transposedBlock">Input block.</param> /// <param name="transposedBlock">Input block.</param>
private static void IDCT_Vector4(ref Block8x8F transposedBlock) 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 // First pass - process columns
IDCT8x4_Vector4(ref transposedBlock.V0L); IDCT8x4_Vector4(ref transposedBlock.V0L);
IDCT8x4_Vector4(ref transposedBlock.V0R); IDCT8x4_Vector4(ref transposedBlock.V0R);
@ -215,136 +217,17 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
} }
} }
/// <summary>
/// Apply 2D floating point FDCT inplace using scalar operations.
/// </summary>
/// <remarks>
/// Ported from libjpeg-turbo https://github.com/libjpeg-turbo/libjpeg-turbo/blob/main/jfdctflt.c.
/// </remarks>
/// <param name="block">Input block.</param>
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<Block8x8F, float>(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<Block8x8F, float>(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);
}
}
/// <summary> /// <summary>
/// Apply floating point FDCT inplace using <see cref="Vector4"/> API. /// Apply floating point FDCT inplace using <see cref="Vector4"/> API.
/// </summary> /// </summary>
/// <remarks>
/// This implementation must be called only if hardware supports 4
/// floating point numbers vector. Otherwise explicit scalar
/// implementation <see cref="FDCT_Scalar"/> is faster
/// because it does not rely on block transposition.
/// </remarks>
/// <param name="block">Input block.</param> /// <param name="block">Input block.</param>
public static void FDCT_Vector4(ref Block8x8F 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 columns
// First pass - process rows
block.TransposeInplace();
FDCT8x4_Vector4(ref block.V0L); FDCT8x4_Vector4(ref block.V0L);
FDCT8x4_Vector4(ref block.V0R); FDCT8x4_Vector4(ref block.V0R);
// Second pass - process columns // Second pass - process rows
block.TransposeInplace(); block.TransposeInplace();
FDCT8x4_Vector4(ref block.V0L); FDCT8x4_Vector4(ref block.V0L);
FDCT8x4_Vector4(ref block.V0R); FDCT8x4_Vector4(ref block.V0R);

429
src/ImageSharp/Formats/Jpeg/Components/ZigZag.Intrinsic.cs

@ -3,6 +3,7 @@
#if SUPPORTS_RUNTIME_INTRINSICS #if SUPPORTS_RUNTIME_INTRINSICS
using System; using System;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics; using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86; using System.Runtime.Intrinsics.X86;
@ -18,120 +19,138 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
#pragma warning restore SA1309 #pragma warning restore SA1309
/// <summary> /// <summary>
/// Gets shuffle vectors for <see cref="ApplyZigZagOrderingSsse3"/> /// Gets shuffle vectors for <see cref="ApplyTransposingZigZagOrderingSsse3"/>
/// zig zag implementation. /// zig zag implementation.
/// </summary> /// </summary>
private static ReadOnlySpan<byte> SseShuffleMasks => new byte[] private static ReadOnlySpan<byte> SseShuffleMasks => new byte[]
{ {
// row0 #pragma warning disable SA1515
0, 1, 2, 3, _, _, _, _, _, _, 4, 5, 6, 7, _, _, /* row0 - A0 B0 A1 A2 B1 C0 D0 C1 */
_, _, _, _, 0, 1, _, _, 2, 3, _, _, _, _, 4, 5, // A
_, _, _, _, _, _, 0, 1, _, _, _, _, _, _, _, _, 0, 1, _, _, 2, 3, 4, 5, _, _, _, _, _, _, _, _,
// B
// row1 _, _, 0, 1, _, _, _, _, 2, 3, _, _, _, _, _, _,
_, _, _, _, _, _, _, _, _, _, _, _, 8, 9, 10, 11, // C
2, 3, _, _, _, _, _, _, 4, 5, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 0, 1, _, _, 2, 3,
_, _, 0, 1, _, _, 2, 3, _, _, _, _, _, _, _, _,
/* row1 - B2 A3 A4 B3 C2 D1 E0 F0 */
// row2 // A
_, _, _, _, _, _, 2, 3, _, _, _, _, _, _, 4, 5, _, _, 6, 7, 8, 9, _, _, _, _, _, _, _, _, _, _,
_, _, _, _, _, _, _, _, 0, 1, _, _, 2, 3, _, _, // B
4, 5, _, _, _, _, 6, 7, _, _, _, _, _, _, _, _,
// row3
_, _, _, _, _, _, 12, 13, 14, 15, _, _, _, _, _, _, /* row2 - E1 D2 C3 B4 A5 A6 B5 C4 */
_, _, _, _, 10, 11, _, _, _, _, 12, 13, _, _, _, _, // A
_, _, 8, 9, _, _, _, _, _, _, _, _, 10, 11, _, _, _, _, _, _, _, _, _, _, 10, 11, 12, 13, _, _, _, _,
6, 7, _, _, _, _, _, _, _, _, _, _, _, _, 8, 9, // B
_, _, _, _, _, _, 8, 9, _, _, _, _, 10, 11, _, _,
// row4 // C
_, _, 4, 5, _, _, _, _, _, _, _, _, 6, 7, _, _, _, _, _, _, 6, 7, _, _, _, _, _, _, _, _, 8, 9,
_, _, _, _, 2, 3, _, _, _, _, 4, 5, _, _, _, _,
_, _, _, _, _, _, 0, 1, 2, 3, _, _, _, _, _, _, /* row3 - D3 E2 F1 G0 H0 G1 F2 E3 */
// E
// row5 _, _, 4, 5, _, _, _, _, _, _, _, _, _, _, 6, 7,
_, _, 12, 13, _, _, 14, 15, _, _, _, _, _, _, _, _, // F
10, 11, _, _, _, _, _, _, 12, 13, _, _, _, _, _, _, _, _, _, _, 2, 3, _, _, _, _, _, _, 4, 5, _, _,
// G
// row6 _, _, _, _, _, _, 0, 1, _, _, 2, 3, _, _, _, _,
_, _, _, _, _, _, _, _, 12, 13, _, _, 14, 15, _, _,
_, _, _, _, _, _, 10, 11, _, _, _, _, _, _, 12, 13, /* row4 - D4 C5 B6 A7 B7 C6 D5 E4 */
4, 5, 6, 7, _, _, _, _, _, _, _, _, _, _, _, _, // B
_, _, _, _, 12, 13, _, _, 14, 15, _, _, _, _, _, _,
// row7 // C
10, 11, _, _, _, _, 12, 13, _, _, 14, 15, _, _, _, _, _, _, 10, 11, _, _, _, _, _, _, 12, 13, _, _, _, _,
_, _, 8, 9, 10, 11, _, _, _, _, _, _, 12, 13, 14, 15 // 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
}; };
/// <summary> /// <summary>
/// Gets shuffle vectors for <see cref="ApplyZigZagOrderingAvx2"/> /// Gets shuffle vectors for <see cref="ApplyTransposingZigZagOrderingAvx2"/>
/// zig zag implementation. /// zig zag implementation.
/// </summary> /// </summary>
private static ReadOnlySpan<byte> AvxShuffleMasks => new byte[] private static ReadOnlySpan<byte> AvxShuffleMasks => new byte[]
{ {
// 01_AB/01_EF/23_CD - cross-lane #pragma warning disable SA1515
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 */
// [cr] crln_01_AB_CD
// 01_AB - inner-lane 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,
0, 1, 2, 3, 8, 9, _, _, 10, 11, 4, 5, 6, 7, 12, 13, _, _, _, _, _, _, _, _, _, _, 10, 11, 4, 5, 6, 7, // (in) AB
0, 1, 8, 9, 2, 3, 4, 5, 10, 11, _, _, _, _, _, _, 12, 13, 2, 3, 4, 5, 14, 15, _, _, _, _, _, _, _, _,
// 01_CD/23_GH - cross-lane // (in) CD
0, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, _, _, _, _, 0, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 0, 1, 8, 9, 2, 3, _, _, _, _, _, _, _, _, 0, 1, 10, 11, _, _, _, _,
// [cr] crln_01_23_EF_23_CD
// 01_CD - inner-lane 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,
_, _, _, _, _, _, 0, 1, _, _, _, _, _, _, _, _, 2, 3, 8, 9, _, _, 10, 11, 4, 5, _, _, _, _, _, _, // (in) EF
_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 0, 1, 8, 9,
// 01_EF - inner-lane
_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 0, 1, _, _, _, _, _, _, _, _, _, _, /* 23 */
// [cr] crln_23_AB_23_45_GH
// 23_AB/45_CD/67_EF - cross-lane 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,
3, 0, 0, 0, 6, 0, 0, 0, 7, 0, 0, 0, _, _, _, _, 3, 0, 0, 0, 6, 0, 0, 0, 7, 0, 0, 0, _, _, _, _, // (in) AB
_, _, _, _, _, _, 8, 9, 2, 3, 4, 5, 10, 11, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _,
// 23_AB - inner-lane // (in) CDe
4, 5, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 6, 7, 0, 1, 2, 3, 8, 9, _, _, _, _, _, _, 12, 13, 6, 7, _, _, _, _, _, _, _, _, 8, 9, 14, 15, _, _, _, _, _, _, _, _, _, _, _, _, _, _,
// (in) EF
// 23_CD - inner-lane 2, 3, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 4, 5, 10, 11, _, _, _, _, _, _, 12, 13, 6, 7,
_, _, 6, 7, 12, 13, _, _, _, _, _, _, _, _, _, _, 10, 11, 4, 5, _, _, _, _, _, _, _, _, 6, 7, 12, 13, // (in) GH
_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 0, 1, 8, 9, 2, 3, _, _, _, _,
// 23_EF - inner-lane
_, _, _, _, _, _, 2, 3, 8, 9, _, _, 10, 11, 4, 5, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, /* 45 */
// (in) AB
// 23_GH - inner-lane _, _, _, _, 12, 13, 6, 7, 14, 15, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _,
_, _, _, _, _, _, _, _, _, _, 0, 1, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, // [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,
// 45_AB - inner-lane // (in) CD
_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 10, 11, _, _, _, _, _, _, _, _, _, _, 8, 9, 2, 3, _, _, _, _, _, _, 4, 5, 10, 11, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 12, 13,
// (in) EF
// 45_CD - inner-lane _, _, _, _, _, _, _, _, _, _, _, _, _, _, 0, 1, 6, 7, _, _, _, _, _, _, _, _, 8, 9, 2, 3, _, _,
_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 6, 7, 0, 1, _, _, 2, 3, 8, 9, _, _, _, _, _, _, // (in) GH
_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 4, 5, 10, 11, 12, 13, 6, 7, _, _, _, _, _, _,
// 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, /* 67 */
// (in) CD
// 45_EF - inner-lane 6, 7, 14, 15, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _,
2, 3, 8, 9, _, _, _, _, _, _, _, _, 10, 11, 4, 5, _, _, _, _, _, _, _, _, _, _, 2, 3, 8, 9, _, _, // [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, _, _, _, _,
// 45_GH - inner-lane // (in) EF
_, _, _, _, 2, 3, 8, 9, 10, 11, 4, 5, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, 6, 7, _, _, _, _, 4, 5, 14, 15, _, _, _, _, _, _, _, _, 8, 9, 2, 3, 10, 11, _, _, _, _, _, _, _, _, _, _,
// (in) GH
// 67_CD - inner-lane _, _, _, _, _, _, _, _, 0, 1, 10, 11, 12, 13, 2, 3, _, _, _, _, _, _, 0, 1, 6, 7, 8, 9, 2, 3, 10, 11,
_, _, _, _, _, _, _, _, _, _, 10, 11, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, #pragma warning restore SA1515
// 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
}; };
/// <summary> /// <summary>
/// Applies zig zag ordering for given 8x8 matrix using SSE cpu intrinsics. /// Applies zig zag ordering for given 8x8 matrix using SSE cpu intrinsics.
/// </summary> /// </summary>
/// <param name="block">Input matrix.</param> /// <param name="block">Input matrix.</param>
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!"); DebugGuard.IsTrue(Ssse3.IsSupported, "Ssse3 support is required to run this operation!");
fixed (byte* maskPtr = SseShuffleMasks) fixed (byte* shuffleVectorsPtr = &MemoryMarshal.GetReference(SseShuffleMasks))
{ {
Vector128<byte> rowA = block.V0.AsByte(); Vector128<byte> rowA = block.V0.AsByte();
Vector128<byte> rowB = block.V1.AsByte(); Vector128<byte> rowB = block.V1.AsByte();
@ -142,73 +161,69 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
Vector128<byte> rowG = block.V6.AsByte(); Vector128<byte> rowG = block.V6.AsByte();
Vector128<byte> rowH = block.V7.AsByte(); Vector128<byte> rowH = block.V7.AsByte();
// row0 - A0 A1 B0 C0 B1 A2 A3 B2 // row0 - A0 B0 A1 A2 B1 C0 D0 C1
Vector128<short> rowA0 = Ssse3.Shuffle(rowA, Sse2.LoadVector128(maskPtr + (16 * 0))).AsInt16(); Vector128<short> row0_A = Ssse3.Shuffle(rowA, Sse2.LoadVector128(shuffleVectorsPtr + (16 * 0))).AsInt16();
Vector128<short> rowB0 = Ssse3.Shuffle(rowB, Sse2.LoadVector128(maskPtr + (16 * 1))).AsInt16(); Vector128<short> row0_B = Ssse3.Shuffle(rowB, Sse2.LoadVector128(shuffleVectorsPtr + (16 * 1))).AsInt16();
Vector128<short> row0 = Sse2.Or(rowA0, rowB0); Vector128<short> row0_C = Ssse3.Shuffle(rowC, Sse2.LoadVector128(shuffleVectorsPtr + (16 * 2))).AsInt16();
Vector128<short> rowC0 = Ssse3.Shuffle(rowC, Sse2.LoadVector128(maskPtr + (16 * 2))).AsInt16(); Vector128<short> row0 = Sse2.Or(Sse2.Or(row0_A, row0_B), row0_C);
row0 = Sse2.Or(row0, rowC0); row0 = Sse2.Insert(row0.AsUInt16(), Sse2.Extract(rowD.AsUInt16(), 0), 6).AsInt16();
// row1 - C1 D0 E0 D1 C2 B3 A4 A5 // row1 - B2 A3 A4 B3 C2 D1 E0 F0
Vector128<short> rowA1 = Ssse3.Shuffle(rowA, Sse2.LoadVector128(maskPtr + (16 * 3))).AsInt16(); Vector128<short> row1_A = Ssse3.Shuffle(rowA, Sse2.LoadVector128(shuffleVectorsPtr + (16 * 3))).AsInt16();
Vector128<short> rowC1 = Ssse3.Shuffle(rowC, Sse2.LoadVector128(maskPtr + (16 * 4))).AsInt16(); Vector128<short> row1_B = Ssse3.Shuffle(rowB, Sse2.LoadVector128(shuffleVectorsPtr + (16 * 4))).AsInt16();
Vector128<short> row1 = Sse2.Or(rowA1, rowC1); Vector128<short> row1 = Sse2.Or(row1_A, row1_B);
Vector128<short> rowD1 = Ssse3.Shuffle(rowD, Sse2.LoadVector128(maskPtr + (16 * 5))).AsInt16(); row1 = Sse2.Insert(row1.AsUInt16(), Sse2.Extract(rowC.AsUInt16(), 2), 4).AsInt16();
row1 = Sse2.Or(row1, rowD1); row1 = Sse2.Insert(row1.AsUInt16(), Sse2.Extract(rowD.AsUInt16(), 1), 5).AsInt16();
row1 = Sse2.Insert(row1.AsUInt16(), Sse2.Extract(rowB.AsUInt16(), 3), 5).AsInt16(); row1 = Sse2.Insert(row1.AsUInt16(), Sse2.Extract(rowE.AsUInt16(), 0), 6).AsInt16();
row1 = Sse2.Insert(row1.AsUInt16(), Sse2.Extract(rowE.AsUInt16(), 0), 2).AsInt16(); row1 = Sse2.Insert(row1.AsUInt16(), Sse2.Extract(rowF.AsUInt16(), 0), 7).AsInt16();
// row2 // row2 - E1 D2 C3 B4 A5 A6 B5 C4
Vector128<short> rowE2 = Ssse3.Shuffle(rowE, Sse2.LoadVector128(maskPtr + (16 * 6))).AsInt16(); Vector128<short> row2_A = Ssse3.Shuffle(rowA, Sse2.LoadVector128(shuffleVectorsPtr + (16 * 5))).AsInt16();
Vector128<short> rowF2 = Ssse3.Shuffle(rowF, Sse2.LoadVector128(maskPtr + (16 * 7))).AsInt16(); Vector128<short> row2_B = Ssse3.Shuffle(rowB, Sse2.LoadVector128(shuffleVectorsPtr + (16 * 6))).AsInt16();
Vector128<short> row2 = Sse2.Or(rowE2, rowF2); Vector128<short> row2_C = Ssse3.Shuffle(rowC, Sse2.LoadVector128(shuffleVectorsPtr + (16 * 7))).AsInt16();
row2 = Sse2.Insert(row2.AsUInt16(), Sse2.Extract(rowB.AsUInt16(), 4), 0).AsInt16(); Vector128<short> row2 = Sse2.Or(Sse2.Or(row2_A, row2_B), row2_C);
row2 = Sse2.Insert(row2.AsUInt16(), Sse2.Extract(rowC.AsUInt16(), 3), 1).AsInt16(); row2 = Sse2.Insert(row2.AsUInt16(), Sse2.Extract(rowD.AsUInt16(), 2), 1).AsInt16();
row2 = Sse2.Insert(row2.AsUInt16(), Sse2.Extract(rowD.AsUInt16(), 2), 2).AsInt16(); row2 = Sse2.Insert(row2.AsUInt16(), Sse2.Extract(rowE.AsUInt16(), 1), 0).AsInt16();
row2 = Sse2.Insert(row2.AsUInt16(), Sse2.Extract(rowG.AsUInt16(), 0), 5).AsInt16();
// row3 - D3 E2 F1 G0 H0 G1 F2 E3
// row3 Vector128<short> row3_E = Ssse3.Shuffle(rowE, Sse2.LoadVector128(shuffleVectorsPtr + (16 * 8))).AsInt16();
Vector128<short> rowA3 = Ssse3.Shuffle(rowA, Sse2.LoadVector128(maskPtr + (16 * 8))).AsInt16().AsInt16(); Vector128<short> row3_F = Ssse3.Shuffle(rowF, Sse2.LoadVector128(shuffleVectorsPtr + (16 * 9))).AsInt16();
Vector128<short> rowB3 = Ssse3.Shuffle(rowB, Sse2.LoadVector128(maskPtr + (16 * 9))).AsInt16().AsInt16(); Vector128<short> row3_G = Ssse3.Shuffle(rowG, Sse2.LoadVector128(shuffleVectorsPtr + (16 * 10))).AsInt16();
Vector128<short> row3 = Sse2.Or(rowA3, rowB3); Vector128<short> row3 = Sse2.Or(Sse2.Or(row3_E, row3_F), row3_G);
Vector128<short> rowC3 = Ssse3.Shuffle(rowC, Sse2.LoadVector128(maskPtr + (16 * 10))).AsInt16(); row3 = Sse2.Insert(row3.AsUInt16(), Sse2.Extract(rowD.AsUInt16(), 3), 0).AsInt16();
row3 = Sse2.Or(row3, rowC3); row3 = Sse2.Insert(row3.AsUInt16(), Sse2.Extract(rowH.AsUInt16(), 0), 4).AsInt16();
Vector128<byte> shuffleRowD3EF = Sse2.LoadVector128(maskPtr + (16 * 11));
Vector128<short> rowD3 = Ssse3.Shuffle(rowD, shuffleRowD3EF).AsInt16(); // row4 - D4 C5 B6 A7 B7 C6 D5 E4
row3 = Sse2.Or(row3, rowD3); Vector128<short> row4_B = Ssse3.Shuffle(rowB, Sse2.LoadVector128(shuffleVectorsPtr + (16 * 11))).AsInt16();
Vector128<short> row4_C = Ssse3.Shuffle(rowC, Sse2.LoadVector128(shuffleVectorsPtr + (16 * 12))).AsInt16();
// row4 Vector128<short> row4_D = Ssse3.Shuffle(rowD, Sse2.LoadVector128(shuffleVectorsPtr + (16 * 13))).AsInt16();
Vector128<short> rowE4 = Ssse3.Shuffle(rowE, shuffleRowD3EF).AsInt16(); Vector128<short> row4 = Sse2.Or(Sse2.Or(row4_B, row4_C), row4_D);
Vector128<short> rowF4 = Ssse3.Shuffle(rowF, Sse2.LoadVector128(maskPtr + (16 * 12))).AsInt16(); row4 = Sse2.Insert(row4.AsUInt16(), Sse2.Extract(rowA.AsUInt16(), 7), 3).AsInt16();
Vector128<short> row4 = Sse2.Or(rowE4, rowF4); row4 = Sse2.Insert(row4.AsUInt16(), Sse2.Extract(rowE.AsUInt16(), 4), 7).AsInt16();
Vector128<short> rowG4 = Ssse3.Shuffle(rowG, Sse2.LoadVector128(maskPtr + (16 * 13))).AsInt16();
row4 = Sse2.Or(row4, rowG4); // row5 - F3 G2 H1 H2 G3 F4 E5 D6
Vector128<short> rowH4 = Ssse3.Shuffle(rowH, Sse2.LoadVector128(maskPtr + (16 * 14))).AsInt16(); Vector128<short> row5_F = Ssse3.Shuffle(rowF, Sse2.LoadVector128(shuffleVectorsPtr + (16 * 14))).AsInt16();
row4 = Sse2.Or(row4, rowH4); Vector128<short> row5_G = Ssse3.Shuffle(rowG, Sse2.LoadVector128(shuffleVectorsPtr + (16 * 15))).AsInt16();
Vector128<short> row5_H = Ssse3.Shuffle(rowH, Sse2.LoadVector128(shuffleVectorsPtr + (16 * 16))).AsInt16();
// row5 Vector128<short> row5 = Sse2.Or(Sse2.Or(row5_F, row5_G), row5_H);
Vector128<short> rowC5 = Ssse3.Shuffle(rowC, Sse2.LoadVector128(maskPtr + (16 * 15))).AsInt16(); row5 = Sse2.Insert(row5.AsUInt16(), Sse2.Extract(rowD.AsUInt16(), 6), 7).AsInt16();
Vector128<short> rowD5 = Ssse3.Shuffle(rowD, Sse2.LoadVector128(maskPtr + (16 * 16))).AsInt16(); row5 = Sse2.Insert(row5.AsUInt16(), Sse2.Extract(rowE.AsUInt16(), 5), 6).AsInt16();
Vector128<short> row5 = Sse2.Or(rowC5, rowD5);
row5 = Sse2.Insert(row5.AsUInt16(), Sse2.Extract(rowB.AsUInt16(), 7), 2).AsInt16(); // row6 - C7 D7 E6 F5 G4 H3 H4 G5
row5 = Sse2.Insert(row5.AsUInt16(), Sse2.Extract(rowE.AsUInt16(), 5), 5).AsInt16(); Vector128<short> row6_G = Ssse3.Shuffle(rowG, Sse2.LoadVector128(shuffleVectorsPtr + (16 * 17))).AsInt16();
row5 = Sse2.Insert(row5.AsUInt16(), Sse2.Extract(rowF.AsUInt16(), 4), 6).AsInt16(); Vector128<short> row6_H = Ssse3.Shuffle(rowH, Sse2.LoadVector128(shuffleVectorsPtr + (16 * 18))).AsInt16();
row5 = Sse2.Insert(row5.AsUInt16(), Sse2.Extract(rowG.AsUInt16(), 3), 7).AsInt16(); Vector128<short> row6 = Sse2.Or(row6_G, row6_H);
row6 = Sse2.Insert(row6.AsUInt16(), Sse2.Extract(rowC.AsUInt16(), 7), 0).AsInt16();
// row6 row6 = Sse2.Insert(row6.AsUInt16(), Sse2.Extract(rowD.AsUInt16(), 7), 1).AsInt16();
Vector128<short> rowE6 = Ssse3.Shuffle(rowE, Sse2.LoadVector128(maskPtr + (16 * 17))).AsInt16(); row6 = Sse2.Insert(row6.AsUInt16(), Sse2.Extract(rowE.AsUInt16(), 6), 2).AsInt16();
Vector128<short> rowF6 = Ssse3.Shuffle(rowF, Sse2.LoadVector128(maskPtr + (16 * 18))).AsInt16(); row6 = Sse2.Insert(row6.AsUInt16(), Sse2.Extract(rowF.AsUInt16(), 5), 3).AsInt16();
Vector128<short> row6 = Sse2.Or(rowE6, rowF6);
Vector128<short> rowH6 = Ssse3.Shuffle(rowH, Sse2.LoadVector128(maskPtr + (16 * 19))).AsInt16(); // row7 - F6 E7 F7 G6 H5 H6 G7 H7
row6 = Sse2.Or(row6, rowH6); Vector128<short> row7_F = Ssse3.Shuffle(rowF, Sse2.LoadVector128(shuffleVectorsPtr + (16 * 19))).AsInt16();
row6 = Sse2.Insert(row6.AsUInt16(), Sse2.Extract(rowD.AsUInt16(), 7), 5).AsInt16(); Vector128<short> row7_G = Ssse3.Shuffle(rowG, Sse2.LoadVector128(shuffleVectorsPtr + (16 * 20))).AsInt16();
row6 = Sse2.Insert(row6.AsUInt16(), Sse2.Extract(rowG.AsUInt16(), 4), 2).AsInt16(); Vector128<short> row7_H = Ssse3.Shuffle(rowH, Sse2.LoadVector128(shuffleVectorsPtr + (16 * 21))).AsInt16();
Vector128<short> row7 = Sse2.Or(Sse2.Or(row7_F, row7_G), row7_H);
// row7 row7 = Sse2.Insert(row7.AsUInt16(), Sse2.Extract(rowE.AsUInt16(), 7), 1).AsInt16();
Vector128<short> rowG7 = Ssse3.Shuffle(rowG, Sse2.LoadVector128(maskPtr + (16 * 20))).AsInt16();
Vector128<short> rowH7 = Ssse3.Shuffle(rowH, Sse2.LoadVector128(maskPtr + (16 * 21))).AsInt16();
Vector128<short> row7 = Sse2.Or(rowG7, rowH7);
row7 = Sse2.Insert(row7.AsUInt16(), Sse2.Extract(rowF.AsUInt16(), 7), 4).AsInt16();
block.V0 = row0; block.V0 = row0;
block.V1 = row1; 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. /// Applies zig zag ordering for given 8x8 matrix using AVX cpu intrinsics.
/// </summary> /// </summary>
/// <param name="block">Input matrix.</param> /// <param name="block">Input matrix.</param>
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!"); DebugGuard.IsTrue(Avx2.IsSupported, "Avx2 support is required to run this operation!");
fixed (byte* shuffleVectorsPtr = AvxShuffleMasks) fixed (byte* shuffleVectorsPtr = &MemoryMarshal.GetReference(AvxShuffleMasks))
{ {
Vector256<byte> rowsAB = block.V01.AsByte(); Vector256<byte> rowAB = block.V01.AsByte();
Vector256<byte> rowsCD = block.V23.AsByte(); Vector256<byte> rowCD = block.V23.AsByte();
Vector256<byte> rowsEF = block.V45.AsByte(); Vector256<byte> rowEF = block.V45.AsByte();
Vector256<byte> rowsGH = block.V67.AsByte(); Vector256<byte> rowGH = block.V67.AsByte();
// rows 0 1 /* row01 - A0 B0 A1 A2 B1 C0 D0 C1 | B2 A3 A4 B3 C2 D1 E0 F0 */
Vector256<int> rows_AB01_EF01_CD23_shuffleMask = Avx.LoadVector256(shuffleVectorsPtr + (0 * 32)).AsInt32(); Vector256<int> crln_01_AB_CD = Avx.LoadVector256(shuffleVectorsPtr + (0 * 32)).AsInt32();
Vector256<byte> row01_AB = Avx2.PermuteVar8x32(rowsAB.AsInt32(), rows_AB01_EF01_CD23_shuffleMask).AsByte(); Vector256<byte> row01_AB = Avx2.PermuteVar8x32(rowAB.AsInt32(), crln_01_AB_CD).AsByte();
row01_AB = Avx2.Shuffle(row01_AB, Avx.LoadVector256(shuffleVectorsPtr + (1 * 32))).AsByte(); row01_AB = Avx2.Shuffle(row01_AB, Avx.LoadVector256(shuffleVectorsPtr + (1 * 32))).AsByte();
Vector256<byte> row01_CD = Avx2.PermuteVar8x32(rowCD.AsInt32(), crln_01_AB_CD).AsByte();
Vector256<int> rows_CD01_GH23_shuffleMask = Avx.LoadVector256(shuffleVectorsPtr + (2 * 32)).AsInt32(); row01_CD = Avx2.Shuffle(row01_CD, Avx.LoadVector256(shuffleVectorsPtr + (2 * 32))).AsByte();
Vector256<byte> row01_CD = Avx2.PermuteVar8x32(rowsCD.AsInt32(), rows_CD01_GH23_shuffleMask).AsByte(); Vector256<int> crln_01_23_EF_23_CD = Avx.LoadVector256(shuffleVectorsPtr + (3 * 32)).AsInt32();
row01_CD = Avx2.Shuffle(row01_CD, Avx.LoadVector256(shuffleVectorsPtr + (3 * 32))).AsByte(); Vector256<byte> row01_23_EF = Avx2.PermuteVar8x32(rowEF.AsInt32(), crln_01_23_EF_23_CD).AsByte();
Vector256<byte> row01_EF = Avx2.Shuffle(row01_23_EF, Avx.LoadVector256(shuffleVectorsPtr + (4 * 32))).AsByte();
Vector256<byte> row0123_EF = Avx2.PermuteVar8x32(rowsEF.AsInt32(), rows_AB01_EF01_CD23_shuffleMask).AsByte();
Vector256<byte> row01_EF = Avx2.Shuffle(row0123_EF, Avx.LoadVector256(shuffleVectorsPtr + (4 * 32))).AsByte(); Vector256<byte> row01 = Avx2.Or(row01_AB, Avx2.Or(row01_CD, row01_EF));
Vector256<byte> row01 = Avx2.Or(Avx2.Or(row01_AB, row01_CD), row01_EF); /* row23 - E1 D2 C3 B4 A5 A6 B5 C4 | D3 E2 F1 G0 H0 G1 F2 E3 */
Vector256<int> crln_23_AB_23_45_GH = Avx.LoadVector256(shuffleVectorsPtr + (5 * 32)).AsInt32();
// rows 2 3 Vector256<byte> row23_45_AB = Avx2.PermuteVar8x32(rowAB.AsInt32(), crln_23_AB_23_45_GH).AsByte();
Vector256<int> rows_AB23_CD45_EF67_shuffleMask = Avx.LoadVector256(shuffleVectorsPtr + (5 * 32)).AsInt32(); Vector256<byte> row23_AB = Avx2.Shuffle(row23_45_AB, Avx.LoadVector256(shuffleVectorsPtr + (6 * 32))).AsByte();
Vector256<byte> row2345_AB = Avx2.PermuteVar8x32(rowsAB.AsInt32(), rows_AB23_CD45_EF67_shuffleMask).AsByte(); Vector256<byte> row23_CD = Avx2.PermuteVar8x32(rowCD.AsInt32(), crln_01_23_EF_23_CD).AsByte();
Vector256<byte> row23_AB = Avx2.Shuffle(row2345_AB, Avx.LoadVector256(shuffleVectorsPtr + (6 * 32))).AsByte();
Vector256<byte> row23_CD = Avx2.PermuteVar8x32(rowsCD.AsInt32(), rows_AB01_EF01_CD23_shuffleMask).AsByte();
row23_CD = Avx2.Shuffle(row23_CD, Avx.LoadVector256(shuffleVectorsPtr + (7 * 32))).AsByte(); row23_CD = Avx2.Shuffle(row23_CD, Avx.LoadVector256(shuffleVectorsPtr + (7 * 32))).AsByte();
Vector256<byte> row23_EF = Avx2.Shuffle(row01_23_EF, Avx.LoadVector256(shuffleVectorsPtr + (8 * 32))).AsByte();
Vector256<byte> row23_EF = Avx2.Shuffle(row0123_EF, Avx.LoadVector256(shuffleVectorsPtr + (8 * 32))).AsByte(); Vector256<byte> row23_45_GH = Avx2.PermuteVar8x32(rowGH.AsInt32(), crln_23_AB_23_45_GH).AsByte();
Vector256<byte> row23_GH = Avx2.Shuffle(row23_45_GH, Avx.LoadVector256(shuffleVectorsPtr + (9 * 32))).AsByte();
Vector256<byte> row2345_GH = Avx2.PermuteVar8x32(rowsGH.AsInt32(), rows_CD01_GH23_shuffleMask).AsByte();
Vector256<byte> row23_GH = Avx2.Shuffle(row2345_GH, Avx.LoadVector256(shuffleVectorsPtr + (9 * 32)).AsByte());
Vector256<byte> row23 = Avx2.Or(Avx2.Or(row23_AB, row23_CD), Avx2.Or(row23_EF, row23_GH)); Vector256<byte> row23 = Avx2.Or(Avx2.Or(row23_AB, row23_CD), Avx2.Or(row23_EF, row23_GH));
// rows 4 5 /* row45 - D4 C5 B6 A7 B7 C6 D5 E4 | F3 G2 H1 H2 G3 F4 E5 D6 */
Vector256<byte> row45_AB = Avx2.Shuffle(row2345_AB, Avx.LoadVector256(shuffleVectorsPtr + (10 * 32)).AsByte()); Vector256<byte> row45_AB = Avx2.Shuffle(row23_45_AB, Avx.LoadVector256(shuffleVectorsPtr + (10 * 32))).AsByte();
Vector256<byte> row4567_CD = Avx2.PermuteVar8x32(rowsCD.AsInt32(), rows_AB23_CD45_EF67_shuffleMask).AsByte(); Vector256<int> crln_45_67_CD_45_EF = Avx.LoadVector256(shuffleVectorsPtr + (11 * 32)).AsInt32();
Vector256<byte> row45_CD = Avx2.Shuffle(row4567_CD, Avx.LoadVector256(shuffleVectorsPtr + (11 * 32)).AsByte()); Vector256<byte> row45_67_CD = Avx2.PermuteVar8x32(rowCD.AsInt32(), crln_45_67_CD_45_EF).AsByte();
Vector256<byte> row45_CD = Avx2.Shuffle(row45_67_CD, Avx.LoadVector256(shuffleVectorsPtr + (12 * 32))).AsByte();
Vector256<int> rows_EF45_GH67_shuffleMask = Avx.LoadVector256(shuffleVectorsPtr + (12 * 32)).AsInt32(); Vector256<byte> row45_EF = Avx2.PermuteVar8x32(rowEF.AsInt32(), crln_45_67_CD_45_EF).AsByte();
Vector256<byte> row45_EF = Avx2.PermuteVar8x32(rowsEF.AsInt32(), rows_EF45_GH67_shuffleMask).AsByte(); row45_EF = Avx2.Shuffle(row45_EF, Avx.LoadVector256(shuffleVectorsPtr + (13 * 32))).AsByte();
row45_EF = Avx2.Shuffle(row45_EF, Avx.LoadVector256(shuffleVectorsPtr + (13 * 32)).AsByte()); Vector256<byte> row45_GH = Avx2.Shuffle(row23_45_GH, Avx.LoadVector256(shuffleVectorsPtr + (14 * 32))).AsByte();
Vector256<byte> row45_GH = Avx2.Shuffle(row2345_GH, Avx.LoadVector256(shuffleVectorsPtr + (14 * 32)).AsByte());
Vector256<byte> row45 = Avx2.Or(Avx2.Or(row45_AB, row45_CD), Avx2.Or(row45_EF, row45_GH)); Vector256<byte> row45 = Avx2.Or(Avx2.Or(row45_AB, row45_CD), Avx2.Or(row45_EF, row45_GH));
// rows 6 7 /* row67 - C7 D7 E6 F5 G4 H3 H4 G5 | F6 E7 F7 G6 H5 H6 G7 H7 */
Vector256<byte> row67_CD = Avx2.Shuffle(row4567_CD, Avx.LoadVector256(shuffleVectorsPtr + (15 * 32)).AsByte()); Vector256<byte> row67_CD = Avx2.Shuffle(row45_67_CD, Avx.LoadVector256(shuffleVectorsPtr + (15 * 32))).AsByte();
Vector256<int> crln_67_EF_67_GH = Avx.LoadVector256(shuffleVectorsPtr + (16 * 32)).AsInt32();
Vector256<byte> row67_EF = Avx2.PermuteVar8x32(rowsEF.AsInt32(), rows_AB23_CD45_EF67_shuffleMask).AsByte(); Vector256<byte> row67_EF = Avx2.PermuteVar8x32(rowEF.AsInt32(), crln_67_EF_67_GH).AsByte();
row67_EF = Avx2.Shuffle(row67_EF, Avx.LoadVector256(shuffleVectorsPtr + (16 * 32)).AsByte()); row67_EF = Avx2.Shuffle(row67_EF, Avx.LoadVector256(shuffleVectorsPtr + (17 * 32))).AsByte();
Vector256<byte> row67_GH = Avx2.PermuteVar8x32(rowGH.AsInt32(), crln_67_EF_67_GH).AsByte();
Vector256<byte> row67_GH = Avx2.PermuteVar8x32(rowsGH.AsInt32(), rows_EF45_GH67_shuffleMask).AsByte(); row67_GH = Avx2.Shuffle(row67_GH, Avx.LoadVector256(shuffleVectorsPtr + (18 * 32))).AsByte();
row67_GH = Avx2.Shuffle(row67_GH, Avx.LoadVector256(shuffleVectorsPtr + (17 * 32)).AsByte());
Vector256<byte> row67 = Avx2.Or(Avx2.Or(row67_CD, row67_EF), row67_GH); Vector256<byte> row67 = Avx2.Or(row67_CD, Avx2.Or(row67_EF, row67_GH));
block.V01 = row01.AsInt16(); block.V01 = row01.AsInt16();
block.V23 = row23.AsInt16(); block.V23 = row23.AsInt16();

176
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.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc; using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Jpeg namespace SixLabors.ImageSharp.Formats.Jpeg
@ -46,7 +47,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <summary> /// <summary>
/// Whether the image has an EXIF marker. /// Whether the image has an EXIF marker.
/// </summary> /// </summary>
private bool isExif; private bool hasExif;
/// <summary> /// <summary>
/// Contains exif data. /// Contains exif data.
@ -56,7 +57,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <summary> /// <summary>
/// Whether the image has an ICC marker. /// Whether the image has an ICC marker.
/// </summary> /// </summary>
private bool isIcc; private bool hasIcc;
/// <summary> /// <summary>
/// Contains ICC data. /// Contains ICC data.
@ -66,13 +67,23 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <summary> /// <summary>
/// Whether the image has a IPTC data. /// Whether the image has a IPTC data.
/// </summary> /// </summary>
private bool isIptc; private bool hasIptc;
/// <summary> /// <summary>
/// Contains IPTC data. /// Contains IPTC data.
/// </summary> /// </summary>
private byte[] iptcData; private byte[] iptcData;
/// <summary>
/// Whether the image has a XMP data.
/// </summary>
private bool hasXmp;
/// <summary>
/// Contains XMP data.
/// </summary>
private byte[] xmpData;
/// <summary> /// <summary>
/// Contains information about the JFIF marker. /// Contains information about the JFIF marker.
/// </summary> /// </summary>
@ -183,6 +194,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.InitExifProfile(); this.InitExifProfile();
this.InitIccProfile(); this.InitIccProfile();
this.InitIptcProfile(); this.InitIptcProfile();
this.InitXmpProfile();
this.InitDerivedMetadataProperties(); this.InitDerivedMetadataProperties();
return new Image<TPixel>( return new Image<TPixel>(
@ -198,6 +210,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.InitExifProfile(); this.InitExifProfile();
this.InitIccProfile(); this.InitIccProfile();
this.InitIptcProfile(); this.InitIptcProfile();
this.InitXmpProfile();
this.InitDerivedMetadataProperties(); this.InitDerivedMetadataProperties();
Size pixelSize = this.Frame.PixelSize; Size pixelSize = this.Frame.PixelSize;
@ -472,7 +485,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
: JpegColorSpace.Cmyk; : JpegColorSpace.Cmyk;
} }
JpegThrowHelper.ThrowInvalidImageContentException($"Unsupported color mode. Supported component counts 1, 3, and 4; found {componentCount}"); JpegThrowHelper.ThrowNotSupportedComponentCount(componentCount);
return default; return default;
} }
@ -539,7 +552,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// </summary> /// </summary>
private void InitExifProfile() private void InitExifProfile()
{ {
if (this.isExif) if (this.hasExif)
{ {
this.Metadata.ExifProfile = new ExifProfile(this.exifData); this.Metadata.ExifProfile = new ExifProfile(this.exifData);
} }
@ -550,7 +563,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// </summary> /// </summary>
private void InitIccProfile() private void InitIccProfile()
{ {
if (this.isIcc) if (this.hasIcc)
{ {
var profile = new IccProfile(this.iccData); var profile = new IccProfile(this.iccData);
if (profile.CheckIsValid()) if (profile.CheckIsValid())
@ -565,13 +578,25 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// </summary> /// </summary>
private void InitIptcProfile() private void InitIptcProfile()
{ {
if (this.isIptc) if (this.hasIptc)
{ {
var profile = new IptcProfile(this.iptcData); var profile = new IptcProfile(this.iptcData);
this.Metadata.IptcProfile = profile; this.Metadata.IptcProfile = profile;
} }
} }
/// <summary>
/// Initializes the XMP profile.
/// </summary>
private void InitXmpProfile()
{
if (this.hasXmp)
{
var profile = new XmpProfile(this.xmpData);
this.Metadata.XmpProfile = profile;
}
}
/// <summary> /// <summary>
/// Assigns derived metadata properties to <see cref="Metadata"/>, eg. horizontal and vertical resolution if it has a JFIF header. /// Assigns derived metadata properties to <see cref="Metadata"/>, eg. horizontal and vertical resolution if it has a JFIF header.
/// </summary> /// </summary>
@ -583,7 +608,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.Metadata.VerticalResolution = this.jFif.YDensity; this.Metadata.VerticalResolution = this.jFif.YDensity;
this.Metadata.ResolutionUnits = this.jFif.DensityUnits; this.Metadata.ResolutionUnits = this.jFif.DensityUnits;
} }
else if (this.isExif) else if (this.hasExif)
{ {
double horizontalValue = this.GetExifResolutionValue(ExifTag.XResolution); double horizontalValue = this.GetExifResolutionValue(ExifTag.XResolution);
double verticalValue = this.GetExifResolutionValue(ExifTag.YResolution); double verticalValue = this.GetExifResolutionValue(ExifTag.YResolution);
@ -625,7 +650,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
private void ProcessApplicationHeaderMarker(BufferedReadStream stream, int remaining) private void ProcessApplicationHeaderMarker(BufferedReadStream stream, int remaining)
{ {
// We can only decode JFif identifiers. // 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 // Skip the application header length
stream.Skip(remaining); stream.Skip(remaining);
@ -656,8 +683,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <param name="remaining">The remaining bytes in the segment block.</param> /// <param name="remaining">The remaining bytes in the segment block.</param>
private void ProcessApp1Marker(BufferedReadStream stream, int remaining) private void ProcessApp1Marker(BufferedReadStream stream, int remaining)
{ {
const int Exif00 = 6; const int ExifMarkerLength = 6;
if (remaining < Exif00 || this.IgnoreMetadata) const int XmpMarkerLength = 29;
if (remaining < ExifMarkerLength || this.IgnoreMetadata)
{ {
// Skip the application header length // Skip the application header length
stream.Skip(remaining); stream.Skip(remaining);
@ -669,23 +697,55 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
JpegThrowHelper.ThrowInvalidImageContentException("Bad App1 Marker length."); JpegThrowHelper.ThrowInvalidImageContentException("Bad App1 Marker length.");
} }
byte[] profile = new byte[remaining]; // XMP marker is the longest, so read at least that many bytes into temp.
stream.Read(profile, 0, remaining); 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) if (this.exifData is null)
{ {
// The first 6 bytes (Exif00) will be skipped, because this is Jpeg specific this.exifData = profile;
this.exifData = profile.AsSpan(Exif00).ToArray();
} }
else else
{ {
// If the EXIF information exceeds 64K, it will be split over multiple APP1 markers // If the EXIF information exceeds 64K, it will be split over multiple APP1 markers
this.ExtendProfile(ref this.exifData, profile.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);
} }
/// <summary> /// <summary>
@ -709,7 +769,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
if (ProfileResolver.IsProfile(identifier, ProfileResolver.IccMarker)) if (ProfileResolver.IsProfile(identifier, ProfileResolver.IccMarker))
{ {
this.isIcc = true; this.hasIcc = true;
byte[] profile = new byte[remaining]; byte[] profile = new byte[remaining];
stream.Read(profile, 0, remaining); stream.Read(profile, 0, remaining);
@ -768,7 +828,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
int dataStartIdx = 2 + resourceBlockNameLength + 4; int dataStartIdx = 2 + resourceBlockNameLength + 4;
if (resourceDataSize > 0 && blockDataSpan.Length >= dataStartIdx + resourceDataSize) if (resourceDataSize > 0 && blockDataSpan.Length >= dataStartIdx + resourceDataSize)
{ {
this.isIptc = true; this.hasIptc = true;
this.iptcData = blockDataSpan.Slice(dataStartIdx, resourceDataSize).ToArray(); this.iptcData = blockDataSpan.Slice(dataStartIdx, resourceDataSize).ToArray();
break; 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);
}
} }
/// <summary> /// <summary>
@ -998,6 +1063,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
// 1 byte: Number of components // 1 byte: Number of components
byte componentCount = this.temp[5]; 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); this.Frame = new JpegFrame(frameMarker, precision, frameWidth, frameHeight, componentCount);
remaining -= length; remaining -= length;
@ -1087,12 +1160,18 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <param name="remaining">The remaining bytes in the segment block.</param> /// <param name="remaining">The remaining bytes in the segment block.</param>
private void ProcessDefineHuffmanTablesMarker(BufferedReadStream stream, int remaining) 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<byte> huffmanData = this.Configuration.MemoryAllocator.Allocate<byte>(256, AllocationOptions.Clean)) int length = remaining;
using (IMemoryOwner<byte> buffer = this.Configuration.MemoryAllocator.Allocate<byte>(totalBufferSize))
{ {
Span<byte> huffmanDataSpan = huffmanData.GetSpan(); Span<byte> bufferSpan = buffer.GetSpan();
ref byte huffmanDataRef = ref MemoryMarshal.GetReference(huffmanDataSpan); Span<byte> huffmanLegthsSpan = bufferSpan.Slice(0, codeLengthsByteSize);
Span<byte> huffmanValuesSpan = bufferSpan.Slice(codeLengthsByteSize, codeValuesMaxByteSize);
Span<uint> tableWorkspace = MemoryMarshal.Cast<byte, uint>(bufferSpan.Slice(codeLengthsByteSize + codeValuesMaxByteSize));
for (int i = 2; i < remaining;) for (int i = 2; i < remaining;)
{ {
byte huffmanTableSpec = (byte)stream.ReadByte(); byte huffmanTableSpec = (byte)stream.ReadByte();
@ -1102,49 +1181,40 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
// Types 0..1 DC..AC // Types 0..1 DC..AC
if (tableType > 1) if (tableType > 1)
{ {
JpegThrowHelper.ThrowInvalidImageContentException($"Bad huffman table type: {tableType}"); JpegThrowHelper.ThrowInvalidImageContentException($"Bad huffman table type: {tableType}.");
} }
// Max tables of each type // Max tables of each type
if (tableIndex > 3) 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<byte> codeLengths = this.Configuration.MemoryAllocator.Allocate<byte>(17, AllocationOptions.Clean)) int codeLengthSum = 0;
for (int j = 1; j < 17; j++)
{ {
Span<byte> codeLengthsSpan = codeLengths.GetSpan(); codeLengthSum += huffmanLegthsSpan[j];
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);
}
length -= 17; length -= 17;
if (codeLengthSum > 256 || codeLengthSum > length) if (codeLengthSum > 256 || codeLengthSum > length)
{ {
JpegThrowHelper.ThrowInvalidImageContentException("Huffman table has excessive length."); JpegThrowHelper.ThrowInvalidImageContentException("Huffman table has excessive length.");
} }
using (IMemoryOwner<byte> huffmanValues = this.Configuration.MemoryAllocator.Allocate<byte>(256, AllocationOptions.Clean)) stream.Read(huffmanValuesSpan, 0, codeLengthSum);
{
Span<byte> huffmanValuesSpan = huffmanValues.GetSpan();
stream.Read(huffmanValuesSpan, 0, codeLengthSum);
i += 17 + codeLengthSum; i += 17 + codeLengthSum;
this.scanDecoder.BuildHuffmanTable( this.scanDecoder.BuildHuffmanTable(
tableType, tableType,
tableIndex, tableIndex,
codeLengthsSpan, huffmanLegthsSpan,
huffmanValuesSpan); huffmanValuesSpan.Slice(0, codeLengthSum),
} tableWorkspace);
}
} }
} }
} }

57
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.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc; using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Jpeg namespace SixLabors.ImageSharp.Formats.Jpeg
@ -109,7 +110,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.WriteJfifApplicationHeader(metadata); this.WriteJfifApplicationHeader(metadata);
} }
// Write Exif, ICC and IPTC profiles // Write Exif, XMP, ICC and IPTC profiles
this.WriteProfiles(metadata); this.WriteProfiles(metadata);
if (this.colorType == JpegColorType.Rgb) if (this.colorType == JpegColorType.Rgb)
@ -466,6 +467,54 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.outputStream.Write(data, 0, data.Length); this.outputStream.Write(data, 0, data.Length);
} }
/// <summary>
/// Writes the XMP metadata.
/// </summary>
/// <param name="xmpProfile">The XMP metadata to write.</param>
/// <exception cref="ImageFormatException">
/// Thrown if the XMP profile size exceeds the limit of 65533 bytes.
/// </exception>
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;
}
}
/// <summary> /// <summary>
/// Writes the App1 header. /// Writes the App1 header.
/// </summary> /// </summary>
@ -579,8 +628,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
return; return;
} }
// For compatibility, place the profiles in the following order:
// - APP1 EXIF
// - APP1 XMP
// - APP2 ICC
// - APP13 IPTC
metadata.SyncProfiles(); metadata.SyncProfiles();
this.WriteExifProfile(metadata.ExifProfile); this.WriteExifProfile(metadata.ExifProfile);
this.WriteXmpProfile(metadata.XmpProfile);
this.WriteIccProfile(metadata.IccProfile); this.WriteIccProfile(metadata.IccProfile);
this.WriteIptcProfile(metadata.IptcProfile); this.WriteIptcProfile(metadata.IptcProfile);
} }

3
src/ImageSharp/Formats/Jpeg/JpegThrowHelper.cs

@ -45,5 +45,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
[MethodImpl(InliningOptions.ColdPath)] [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."); 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.");
} }
} }

13
src/ImageSharp/Formats/PixelTypeInfo.cs

@ -15,8 +15,17 @@ namespace SixLabors.ImageSharp.Formats
/// Initializes a new instance of the <see cref="PixelTypeInfo"/> class. /// Initializes a new instance of the <see cref="PixelTypeInfo"/> class.
/// </summary> /// </summary>
/// <param name="bitsPerPixel">Color depth, in number of bits per pixel.</param> /// <param name="bitsPerPixel">Color depth, in number of bits per pixel.</param>
/// <param name="alpha">Tthe pixel alpha transparency behavior.</param> public PixelTypeInfo(int bitsPerPixel)
internal PixelTypeInfo(int bitsPerPixel, PixelAlphaRepresentation? alpha = null) {
this.BitsPerPixel = bitsPerPixel;
}
/// <summary>
/// Initializes a new instance of the <see cref="PixelTypeInfo"/> class.
/// </summary>
/// <param name="bitsPerPixel">Color depth, in number of bits per pixel.</param>
/// <param name="alpha">The pixel alpha transparency behavior.</param>
public PixelTypeInfo(int bitsPerPixel, PixelAlphaRepresentation alpha)
{ {
this.BitsPerPixel = bitsPerPixel; this.BitsPerPixel = bitsPerPixel;
this.AlphaRepresentation = alpha; this.AlphaRepresentation = alpha;

24
src/ImageSharp/Formats/Png/PngConstants.cs

@ -78,5 +78,29 @@ namespace SixLabors.ImageSharp.Formats.Png
0x1A, // EOF 0x1A, // EOF
0x0A // LF 0x0A // LF
}; };
/// <summary>
/// Gets the keyword of the XMP metadata, encoded in an iTXT chunk.
/// </summary>
public static ReadOnlySpan<byte> 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'
};
} }
} }

218
src/ImageSharp/Formats/Png/PngDecoderCore.cs

@ -11,6 +11,7 @@ using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Compression.Zlib; using SixLabors.ImageSharp.Compression.Zlib;
using SixLabors.ImageSharp.Formats.Png.Chunks; using SixLabors.ImageSharp.Formats.Png.Chunks;
using SixLabors.ImageSharp.Formats.Png.Filters; using SixLabors.ImageSharp.Formats.Png.Filters;
@ -18,6 +19,7 @@ using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Png namespace SixLabors.ImageSharp.Formats.Png
@ -187,20 +189,20 @@ namespace SixLabors.ImageSharp.Formats.Png
this.AssignTransparentMarkers(alpha, pngMetadata); this.AssignTransparentMarkers(alpha, pngMetadata);
break; break;
case PngChunkType.Text: case PngChunkType.Text:
this.ReadTextChunk(pngMetadata, chunk.Data.GetSpan()); this.ReadTextChunk(metadata, pngMetadata, chunk.Data.GetSpan());
break; break;
case PngChunkType.CompressedText: case PngChunkType.CompressedText:
this.ReadCompressedTextChunk(pngMetadata, chunk.Data.GetSpan()); this.ReadCompressedTextChunk(metadata, pngMetadata, chunk.Data.GetSpan());
break; break;
case PngChunkType.InternationalText: case PngChunkType.InternationalText:
this.ReadInternationalTextChunk(pngMetadata, chunk.Data.GetSpan()); this.ReadInternationalTextChunk(metadata, chunk.Data.GetSpan());
break; break;
case PngChunkType.Exif: case PngChunkType.Exif:
if (!this.ignoreMetadata) if (!this.ignoreMetadata)
{ {
byte[] exifData = new byte[chunk.Length]; byte[] exifData = new byte[chunk.Length];
chunk.Data.GetSpan().CopyTo(exifData); chunk.Data.GetSpan().CopyTo(exifData);
metadata.ExifProfile = new ExifProfile(exifData); this.MergeOrSetExifProfile(metadata, new ExifProfile(exifData), replaceExistingKeys: true);
} }
break; break;
@ -297,7 +299,7 @@ namespace SixLabors.ImageSharp.Formats.Png
break; break;
} }
this.ReadTextChunk(pngMetadata, chunk.Data.GetSpan()); this.ReadTextChunk(metadata, pngMetadata, chunk.Data.GetSpan());
break; break;
case PngChunkType.CompressedText: case PngChunkType.CompressedText:
if (this.colorMetadataOnly) if (this.colorMetadataOnly)
@ -306,7 +308,7 @@ namespace SixLabors.ImageSharp.Formats.Png
break; break;
} }
this.ReadCompressedTextChunk(pngMetadata, chunk.Data.GetSpan()); this.ReadCompressedTextChunk(metadata, pngMetadata, chunk.Data.GetSpan());
break; break;
case PngChunkType.InternationalText: case PngChunkType.InternationalText:
if (this.colorMetadataOnly) if (this.colorMetadataOnly)
@ -315,7 +317,7 @@ namespace SixLabors.ImageSharp.Formats.Png
break; break;
} }
this.ReadInternationalTextChunk(pngMetadata, chunk.Data.GetSpan()); this.ReadInternationalTextChunk(metadata, chunk.Data.GetSpan());
break; break;
case PngChunkType.Exif: case PngChunkType.Exif:
if (this.colorMetadataOnly) if (this.colorMetadataOnly)
@ -328,7 +330,7 @@ namespace SixLabors.ImageSharp.Formats.Png
{ {
byte[] exifData = new byte[chunk.Length]; byte[] exifData = new byte[chunk.Length];
chunk.Data.GetSpan().CopyTo(exifData); chunk.Data.GetSpan().CopyTo(exifData);
metadata.ExifProfile = new ExifProfile(exifData); this.MergeOrSetExifProfile(metadata, new ExifProfile(exifData), replaceExistingKeys: true);
} }
break; break;
@ -967,9 +969,10 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <summary> /// <summary>
/// Reads a text chunk containing image properties from the data. /// Reads a text chunk containing image properties from the data.
/// </summary> /// </summary>
/// <param name="baseMetadata">The <see cref="ImageMetadata"/> object.</param>
/// <param name="metadata">The metadata to decode to.</param> /// <param name="metadata">The metadata to decode to.</param>
/// <param name="data">The <see cref="T:Span"/> containing the data.</param> /// <param name="data">The <see cref="T:Span"/> containing the data.</param>
private void ReadTextChunk(PngMetadata metadata, ReadOnlySpan<byte> data) private void ReadTextChunk(ImageMetadata baseMetadata, PngMetadata metadata, ReadOnlySpan<byte> data)
{ {
if (this.ignoreMetadata) if (this.ignoreMetadata)
{ {
@ -992,15 +995,19 @@ namespace SixLabors.ImageSharp.Formats.Png
string value = PngConstants.Encoding.GetString(data.Slice(zeroIndex + 1)); 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));
}
} }
/// <summary> /// <summary>
/// Reads the compressed text chunk. Contains a uncompressed keyword and a compressed text string. /// Reads the compressed text chunk. Contains a uncompressed keyword and a compressed text string.
/// </summary> /// </summary>
/// <param name="baseMetadata">The <see cref="ImageMetadata"/> object.</param>
/// <param name="metadata">The metadata to decode to.</param> /// <param name="metadata">The metadata to decode to.</param>
/// <param name="data">The <see cref="T:Span"/> containing the data.</param> /// <param name="data">The <see cref="T:Span"/> containing the data.</param>
private void ReadCompressedTextChunk(PngMetadata metadata, ReadOnlySpan<byte> data) private void ReadCompressedTextChunk(ImageMetadata baseMetadata, PngMetadata metadata, ReadOnlySpan<byte> data)
{ {
if (this.ignoreMetadata) if (this.ignoreMetadata)
{ {
@ -1028,12 +1035,185 @@ namespace SixLabors.ImageSharp.Formats.Png
ReadOnlySpan<byte> compressedData = data.Slice(zeroIndex + 2); ReadOnlySpan<byte> 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)); metadata.TextData.Add(new PngTextData(name, uncompressed, string.Empty, string.Empty));
} }
} }
/// <summary>
/// Checks if the given text chunk is actually storing parsable metadata.
/// </summary>
/// <param name="baseMetadata">The <see cref="ImageMetadata"/> object to store the parsed metadata in.</param>
/// <param name="chunkName">The name of the text chunk.</param>
/// <param name="chunkText">The contents of the text chunk.</param>
/// <returns>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.</returns>
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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="metadata">The <see cref="ImageMetadata"/> to store the decoded exif tags into.</param>
/// <param name="data">The contents of the "raw profile type exif" text chunk.</param>
private bool TryReadLegacyExifTextChunk(ImageMetadata metadata, string data)
{
ReadOnlySpan<char> 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<byte> 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<char> 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;
}
/// <summary>
/// Compares two ReadOnlySpan&lt;char&gt;s in a case-insensitive method.
/// This is only needed because older frameworks are missing the extension method.
/// </summary>
/// <param name="span1">The first <see cref="Span{T}"/> to compare.</param>
/// <param name="span2">The second <see cref="Span{T}"/> to compare.</param>
/// <returns>True if the spans were identical, false otherwise.</returns>
private static bool StringEqualsInsensitive(ReadOnlySpan<char> span1, ReadOnlySpan<char> 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
}
/// <summary>
/// int.Parse() a ReadOnlySpan&lt;char&gt;, with a fallback for older frameworks.
/// </summary>
/// <param name="span">The <see cref="int"/> to parse.</param>
/// <returns>The parsed <see cref="int"/>.</returns>
private static int ParseInt32(ReadOnlySpan<char> 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
}
/// <summary>
/// Sets the <see cref="ExifProfile"/> in <paramref name="metadata"/> to <paramref name="newProfile"/>,
/// or copies exif tags if <paramref name="metadata"/> already contains an <see cref="ExifProfile"/>.
/// </summary>
/// <param name="metadata">The <see cref="ImageMetadata"/> to store the exif data in.</param>
/// <param name="newProfile">The <see cref="ExifProfile"/> to copy exif tags from.</param>
/// <param name="replaceExistingKeys">If <paramref name="metadata"/> already contains an <see cref="ExifProfile"/>,
/// controls whether existing exif tags in <paramref name="metadata"/> will be overwritten with any conflicting
/// tags from <paramref name="newProfile"/>.</param>
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());
}
}
}
}
/// <summary> /// <summary>
/// Reads a iTXt chunk, which contains international text data. It contains: /// Reads a iTXt chunk, which contains international text data. It contains:
/// - A uncompressed keyword. /// - A uncompressed keyword.
@ -1045,13 +1225,14 @@ namespace SixLabors.ImageSharp.Formats.Png
/// </summary> /// </summary>
/// <param name="metadata">The metadata to decode to.</param> /// <param name="metadata">The metadata to decode to.</param>
/// <param name="data">The <see cref="T:Span"/> containing the data.</param> /// <param name="data">The <see cref="T:Span"/> containing the data.</param>
private void ReadInternationalTextChunk(PngMetadata metadata, ReadOnlySpan<byte> data) private void ReadInternationalTextChunk(ImageMetadata metadata, ReadOnlySpan<byte> data)
{ {
if (this.ignoreMetadata) if (this.ignoreMetadata)
{ {
return; return;
} }
PngMetadata pngMetadata = metadata.GetPngMetadata();
int zeroIndexKeyword = data.IndexOf((byte)0); int zeroIndexKeyword = data.IndexOf((byte)0);
if (zeroIndexKeyword < PngConstants.MinTextKeywordLength || zeroIndexKeyword > PngConstants.MaxTextKeywordLength) 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)) 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 else
{ {
string value = PngConstants.TranslatedEncoding.GetString(data.Slice(dataStartIdx)); 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; return true;
} }
private bool IsXmpTextData(ReadOnlySpan<byte> keywordBytes) => keywordBytes.SequenceEqual(PngConstants.XmpKeyword);
private void SwapScanlineBuffers() private void SwapScanlineBuffers()
{ {
IMemoryOwner<byte> temp = this.previousScanline; IMemoryOwner<byte> temp = this.previousScanline;

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

@ -138,6 +138,7 @@ namespace SixLabors.ImageSharp.Formats.Png
this.WriteTransparencyChunk(stream, pngMetadata); this.WriteTransparencyChunk(stream, pngMetadata);
this.WritePhysicalChunk(stream, metadata); this.WritePhysicalChunk(stream, metadata);
this.WriteExifChunk(stream, metadata); this.WriteExifChunk(stream, metadata);
this.WriteXmpChunk(stream, metadata);
this.WriteTextChunks(stream, pngMetadata); this.WriteTextChunks(stream, pngMetadata);
this.WriteDataChunks(clearTransparency ? clonedImage : image, quantized, stream); this.WriteDataChunks(clearTransparency ? clonedImage : image, quantized, stream);
this.WriteEndChunk(stream); this.WriteEndChunk(stream);
@ -654,6 +655,51 @@ namespace SixLabors.ImageSharp.Formats.Png
this.WriteChunk(stream, PngChunkType.Exif, meta.ExifProfile.ToByteArray()); this.WriteChunk(stream, PngChunkType.Exif, meta.ExifProfile.ToByteArray());
} }
/// <summary>
/// Writes an iTXT chunk, containing the XMP metdata to the stream, if such profile is present in the metadata.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <param name="meta">The image metadata.</param>
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<byte> owner = this.memoryAllocator.Allocate<byte>(payloadLength))
{
Span<byte> 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);
}
}
/// <summary> /// <summary>
/// Writes a text chunk to the stream. Can be either a tTXt, iTXt or zTXt chunk, /// 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. /// 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[] translatedKeyword = PngConstants.TranslatedEncoding.GetBytes(textData.TranslatedKeyword);
byte[] languageTag = PngConstants.LanguageEncoding.GetBytes(textData.LanguageTag); byte[] languageTag = PngConstants.LanguageEncoding.GetBytes(textData.LanguageTag);
Span<byte> outputBytes = new byte[keywordBytes.Length + textBytes.Length + int payloadLength = keywordBytes.Length + textBytes.Length + translatedKeyword.Length + languageTag.Length + 5;
translatedKeyword.Length + languageTag.Length + 5]; using (IMemoryOwner<byte> owner = this.memoryAllocator.Allocate<byte>(payloadLength))
keywordBytes.CopyTo(outputBytes);
if (textData.Value.Length > this.options.TextCompressionThreshold)
{ {
// Indicate that the text is compressed. Span<byte> outputBytes = owner.GetSpan();
outputBytes[keywordBytes.Length + 1] = 1; 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; outputBytes[bytesWritten++] = 0;
languageTag.CopyTo(outputBytes.Slice(keywordStart)); languageTag.CopyTo(outputBytes.Slice(bytesWritten));
int translatedKeywordStart = keywordStart + languageTag.Length + 1; bytesWritten += languageTag.Length;
translatedKeyword.CopyTo(outputBytes.Slice(translatedKeywordStart)); outputBytes[bytesWritten++] = 0;
textBytes.CopyTo(outputBytes.Slice(translatedKeywordStart + translatedKeyword.Length + 1)); translatedKeyword.CopyTo(outputBytes.Slice(bytesWritten));
this.WriteChunk(stream, PngChunkType.InternationalText, outputBytes.ToArray()); bytesWritten += translatedKeyword.Length;
outputBytes[bytesWritten++] = 0;
textBytes.CopyTo(outputBytes.Slice(bytesWritten));
this.WriteChunk(stream, PngChunkType.InternationalText, outputBytes);
}
} }
else else
{ {
@ -716,19 +774,32 @@ namespace SixLabors.ImageSharp.Formats.Png
// Write zTXt chunk. // Write zTXt chunk.
byte[] compressedData = byte[] compressedData =
this.GetCompressedTextBytes(PngConstants.Encoding.GetBytes(textData.Value)); this.GetCompressedTextBytes(PngConstants.Encoding.GetBytes(textData.Value));
Span<byte> outputBytes = new byte[textData.Keyword.Length + compressedData.Length + 2]; int payloadLength = textData.Keyword.Length + compressedData.Length + 2;
PngConstants.Encoding.GetBytes(textData.Keyword).CopyTo(outputBytes); using (IMemoryOwner<byte> owner = this.memoryAllocator.Allocate<byte>(payloadLength))
compressedData.CopyTo(outputBytes.Slice(textData.Keyword.Length + 2)); {
this.WriteChunk(stream, PngChunkType.CompressedText, outputBytes.ToArray()); Span<byte> 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 else
{ {
// Write tEXt chunk. // Write tEXt chunk.
Span<byte> outputBytes = new byte[textData.Keyword.Length + textData.Value.Length + 1]; int payloadLength = textData.Keyword.Length + textData.Value.Length + 1;
PngConstants.Encoding.GetBytes(textData.Keyword).CopyTo(outputBytes); using (IMemoryOwner<byte> owner = this.memoryAllocator.Allocate<byte>(payloadLength))
PngConstants.Encoding.GetBytes(textData.Value) {
.CopyTo(outputBytes.Slice(textData.Keyword.Length + 1)); Span<byte> outputBytes = owner.GetSpan();
this.WriteChunk(stream, PngChunkType.Text, outputBytes.ToArray()); 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());
}
} }
} }
} }

468
src/ImageSharp/Formats/Tiff/Compression/Compressors/T4BitCompressor.cs

@ -2,8 +2,6 @@
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System; using System;
using System.Buffers;
using System.Collections.Generic;
using System.IO; using System.IO;
using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
@ -13,222 +11,35 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors
/// <summary> /// <summary>
/// Bitwriter for writing compressed CCITT T4 1D data. /// Bitwriter for writing compressed CCITT T4 1D data.
/// </summary> /// </summary>
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<uint, uint> WhiteLen4TermCodes = new Dictionary<uint, uint>()
{
{ 2, 0x7 }, { 3, 0x8 }, { 4, 0xB }, { 5, 0xC }, { 6, 0xE }, { 7, 0xF }
};
private static readonly Dictionary<uint, uint> WhiteLen5TermCodes = new Dictionary<uint, uint>()
{
{ 8, 0x13 }, { 9, 0x14 }, { 10, 0x7 }, { 11, 0x8 }
};
private static readonly Dictionary<uint, uint> WhiteLen6TermCodes = new Dictionary<uint, uint>()
{
{ 1, 0x7 }, { 12, 0x8 }, { 13, 0x3 }, { 14, 0x34 }, { 15, 0x35 }, { 16, 0x2A }, { 17, 0x2B }
};
private static readonly Dictionary<uint, uint> WhiteLen7TermCodes = new Dictionary<uint, uint>()
{
{ 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<uint, uint> WhiteLen8TermCodes = new Dictionary<uint, uint>()
{
{ 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<uint, uint> BlackLen2TermCodes = new Dictionary<uint, uint>()
{
{ 2, 0x3 }, { 3, 0x2 }
};
private static readonly Dictionary<uint, uint> BlackLen3TermCodes = new Dictionary<uint, uint>()
{
{ 1, 0x2 }, { 4, 0x3 }
};
private static readonly Dictionary<uint, uint> BlackLen4TermCodes = new Dictionary<uint, uint>()
{
{ 5, 0x3 }, { 6, 0x2 }
};
private static readonly Dictionary<uint, uint> BlackLen5TermCodes = new Dictionary<uint, uint>()
{
{ 7, 0x3 }
};
private static readonly Dictionary<uint, uint> BlackLen6TermCodes = new Dictionary<uint, uint>()
{
{ 8, 0x5 }, { 9, 0x4 }
};
private static readonly Dictionary<uint, uint> BlackLen7TermCodes = new Dictionary<uint, uint>()
{
{ 10, 0x4 }, { 11, 0x5 }, { 12, 0x7 }
};
private static readonly Dictionary<uint, uint> BlackLen8TermCodes = new Dictionary<uint, uint>()
{
{ 13, 0x4 }, { 14, 0x7 }
};
private static readonly Dictionary<uint, uint> BlackLen9TermCodes = new Dictionary<uint, uint>()
{
{ 15, 0x18 }
};
private static readonly Dictionary<uint, uint> BlackLen10TermCodes = new Dictionary<uint, uint>()
{
{ 0, BlackZeroRunTermCode }, { 16, 0x17 }, { 17, 0x18 }, { 18, 0x8 }
};
private static readonly Dictionary<uint, uint> BlackLen11TermCodes = new Dictionary<uint, uint>()
{
{ 19, 0x67 }, { 20, 0x68 }, { 21, 0x6C }, { 22, 0x37 }, { 23, 0x28 }, { 24, 0x17 }, { 25, 0x18 }
};
private static readonly Dictionary<uint, uint> BlackLen12TermCodes = new Dictionary<uint, uint>()
{
{ 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<uint, uint> WhiteLen5MakeupCodes = new Dictionary<uint, uint>()
{
{ 64, 0x1B }, { 128, 0x12 }
};
private static readonly Dictionary<uint, uint> WhiteLen6MakeupCodes = new Dictionary<uint, uint>()
{
{ 192, 0x17 }, { 1664, 0x18 }
};
private static readonly Dictionary<uint, uint> WhiteLen8MakeupCodes = new Dictionary<uint, uint>()
{
{ 320, 0x36 }, { 384, 0x37 }, { 448, 0x64 }, { 512, 0x65 }, { 576, 0x68 }, { 640, 0x67 }
};
private static readonly Dictionary<uint, uint> WhiteLen7MakeupCodes = new Dictionary<uint, uint>()
{
{ 256, 0x37 }
};
private static readonly Dictionary<uint, uint> WhiteLen9MakeupCodes = new Dictionary<uint, uint>()
{
{ 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<uint, uint> WhiteLen11MakeupCodes = new Dictionary<uint, uint>()
{
{ 1792, 0x8 }, { 1856, 0xC }, { 1920, 0xD }
};
private static readonly Dictionary<uint, uint> WhiteLen12MakeupCodes = new Dictionary<uint, uint>()
{
{ 1984, 0x12 }, { 2048, 0x13 }, { 2112, 0x14 }, { 2176, 0x15 }, { 2240, 0x16 }, { 2304, 0x17 }, { 2368, 0x1C },
{ 2432, 0x1D }, { 2496, 0x1E }, { 2560, 0x1F }
};
private static readonly Dictionary<uint, uint> BlackLen10MakeupCodes = new Dictionary<uint, uint>()
{
{ 64, 0xF }
};
private static readonly Dictionary<uint, uint> BlackLen11MakeupCodes = new Dictionary<uint, uint>()
{
{ 1792, 0x8 }, { 1856, 0xC }, { 1920, 0xD }
};
private static readonly Dictionary<uint, uint> BlackLen12MakeupCodes = new Dictionary<uint, uint>()
{
{ 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<uint, uint> BlackLen13MakeupCodes = new Dictionary<uint, uint>()
{
{ 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 }
};
/// <summary> /// <summary>
/// The modified huffman is basically the same as CCITT T4, but without EOL markers and padding at the end of the rows. /// The modified huffman is basically the same as CCITT T4, but without EOL markers and padding at the end of the rows.
/// </summary> /// </summary>
private readonly bool useModifiedHuffman; private readonly bool useModifiedHuffman;
private IMemoryOwner<byte> compressedDataBuffer;
private int bytePosition;
private byte bitPosition;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="T4BitCompressor" /> class. /// Initializes a new instance of the <see cref="T4BitCompressor" /> class.
/// </summary> /// </summary>
/// <param name="output">The output.</param> /// <param name="output">The output stream to write the compressed data.</param>
/// <param name="allocator">The allocator.</param> /// <param name="allocator">The memory allocator.</param>
/// <param name="width">The width.</param> /// <param name="width">The width of the image.</param>
/// <param name="bitsPerPixel">The bits per pixel.</param> /// <param name="bitsPerPixel">The bits per pixel.</param>
/// <param name="useModifiedHuffman">Indicates if the modified huffman RLE should be used.</param> /// <param name="useModifiedHuffman">Indicates if the modified huffman RLE should be used.</param>
public T4BitCompressor(Stream output, MemoryAllocator allocator, int width, int bitsPerPixel, bool useModifiedHuffman = false) public T4BitCompressor(Stream output, MemoryAllocator allocator, int width, int bitsPerPixel, bool useModifiedHuffman = false)
: base(output, allocator, width, bitsPerPixel) : base(output, allocator, width, bitsPerPixel) => this.useModifiedHuffman = useModifiedHuffman;
{
this.bytePosition = 0;
this.bitPosition = 0;
this.useModifiedHuffman = useModifiedHuffman;
}
/// <inheritdoc/> /// <inheritdoc/>
public override TiffCompression Method => this.useModifiedHuffman ? TiffCompression.Ccitt1D : TiffCompression.CcittGroup3Fax; public override TiffCompression Method => this.useModifiedHuffman ? TiffCompression.Ccitt1D : TiffCompression.CcittGroup3Fax;
/// <inheritdoc/>
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<byte>(maxNeededBytes);
}
/// <summary> /// <summary>
/// Writes a image compressed with CCITT T4 to the stream. /// Writes a image compressed with CCITT T4 to the output buffer.
/// </summary> /// </summary>
/// <param name="pixelsAsGray">The pixels as 8-bit gray array.</param> /// <param name="pixelsAsGray">The pixels as 8-bit gray array.</param>
/// <param name="height">The strip height.</param> /// <param name="height">The strip height.</param>
public override void CompressStrip(Span<byte> pixelsAsGray, int height) /// <param name="compressedData">The destination for the compressed data.</param>
protected override void CompressStrip(Span<byte> pixelsAsGray, int height, Span<byte> 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<byte> compressedData = this.compressedDataBuffer.GetSpan();
this.bytePosition = 0;
this.bitPosition = 0;
if (!this.useModifiedHuffman) if (!this.useModifiedHuffman)
{ {
// An EOL code is expected at the start of the data. // 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); 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<byte> compressedData) private void WriteEndOfLine(Span<byte> compressedData)
{ {
if (this.useModifiedHuffman) if (this.useModifiedHuffman)
{ {
// Check if padding is necessary. this.PadByte();
if (this.bitPosition % 8 != 0)
{
// Skip padding bits, move to next byte.
this.bytePosition++;
this.bitPosition = 0;
}
} }
else else
{ {
@ -341,254 +140,5 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Compressors
this.WriteCode(12, 1, compressedData); this.WriteCode(12, 1, compressedData);
} }
} }
private void WriteCode(uint codeLength, uint code, Span<byte> 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;
}
} }
} }

201
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
{
/// <summary>
/// Bitwriter for writing compressed CCITT T6 2D data.
/// </summary>
internal sealed class T6BitCompressor : TiffCcittCompressor
{
/// <summary>
/// Vertical codes from -3 to +3.
/// </summary>
private static readonly (uint Length, uint Code)[] VerticalCodes =
{
(7u, 3u),
(6u, 3u),
(3u, 3u),
(1u, 1u),
(3u, 2u),
(6u, 2u),
(7u, 2u)
};
private IMemoryOwner<byte> referenceLineBuffer;
/// <summary>
/// Initializes a new instance of the <see cref="T6BitCompressor"/> class.
/// </summary>
/// <param name="output">The output stream to write the compressed data.</param>
/// <param name="allocator">The memory allocator.</param>
/// <param name="width">The width of the image.</param>
/// <param name="bitsPerPixel">The bits per pixel.</param>
public T6BitCompressor(Stream output, MemoryAllocator allocator, int width, int bitsPerPixel)
: base(output, allocator, width, bitsPerPixel)
{
}
/// <inheritdoc />
public override TiffCompression Method => TiffCompression.CcittGroup4Fax;
/// <summary>
/// Writes a image compressed with CCITT T6 to the output buffer.
/// </summary>
/// <param name="pixelsAsGray">The pixels as 8-bit gray array.</param>
/// <param name="height">The strip height.</param>
/// <param name="compressedData">The destination for the compressed data.</param>
protected override void CompressStrip(Span<byte> pixelsAsGray, int height, Span<byte> compressedData)
{
// Initial reference line is all white.
Span<byte> referenceLine = this.referenceLineBuffer.GetSpan();
referenceLine.Fill(0xff);
for (int y = 0; y < height; y++)
{
Span<byte> 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);
}
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
this.referenceLineBuffer?.Dispose();
base.Dispose(disposing);
}
/// <summary>
/// Finds the end of a pixel run.
/// </summary>
/// <param name="row">The row of pixels to examine.</param>
/// <param name="startIndex">The index of the first pixel in <paramref name="row"/> to examine.</param>
/// <param name="color">Color of pixels in the run. If not specified, the color at
/// <paramref name="startIndex"/> will be used.</param>
/// <returns>The index of the first pixel at or after <paramref name="startIndex"/>
/// that does not match <paramref name="color"/>, or the length of <paramref name="row"/>,
/// whichever comes first.</returns>
private uint FindRunEnd(Span<byte> 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;
}
/// <inheritdoc />
public override void Initialize(int rowsPerStrip)
{
base.Initialize(rowsPerStrip);
this.referenceLineBuffer = this.Allocator.Allocate<byte>(this.Width);
}
/// <summary>
/// Writes a run to the output buffer.
/// </summary>
/// <param name="runLength">The length of the run.</param>
/// <param name="isWhiteRun">If <c>true</c> the run is white pixels,
/// if <c>false</c> the run is black pixels.</param>
/// <param name="compressedData">The destination to write the run to.</param>
private void WriteRun(uint runLength, bool isWhiteRun, Span<byte> 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);
}
}
}

536
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
{
/// <summary>
/// Common functionality for CCITT T4 and T6 Compression
/// </summary>
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<uint, uint> WhiteLen4TermCodes = new Dictionary<uint, uint>()
{
{ 2, 0x7 }, { 3, 0x8 }, { 4, 0xB }, { 5, 0xC }, { 6, 0xE }, { 7, 0xF }
};
private static readonly Dictionary<uint, uint> WhiteLen5TermCodes = new Dictionary<uint, uint>()
{
{ 8, 0x13 }, { 9, 0x14 }, { 10, 0x7 }, { 11, 0x8 }
};
private static readonly Dictionary<uint, uint> WhiteLen6TermCodes = new Dictionary<uint, uint>()
{
{ 1, 0x7 }, { 12, 0x8 }, { 13, 0x3 }, { 14, 0x34 }, { 15, 0x35 }, { 16, 0x2A }, { 17, 0x2B }
};
private static readonly Dictionary<uint, uint> WhiteLen7TermCodes = new Dictionary<uint, uint>()
{
{ 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<uint, uint> WhiteLen8TermCodes = new Dictionary<uint, uint>()
{
{ 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<uint, uint> BlackLen2TermCodes = new Dictionary<uint, uint>()
{
{ 2, 0x3 }, { 3, 0x2 }
};
private static readonly Dictionary<uint, uint> BlackLen3TermCodes = new Dictionary<uint, uint>()
{
{ 1, 0x2 }, { 4, 0x3 }
};
private static readonly Dictionary<uint, uint> BlackLen4TermCodes = new Dictionary<uint, uint>()
{
{ 5, 0x3 }, { 6, 0x2 }
};
private static readonly Dictionary<uint, uint> BlackLen5TermCodes = new Dictionary<uint, uint>()
{
{ 7, 0x3 }
};
private static readonly Dictionary<uint, uint> BlackLen6TermCodes = new Dictionary<uint, uint>()
{
{ 8, 0x5 }, { 9, 0x4 }
};
private static readonly Dictionary<uint, uint> BlackLen7TermCodes = new Dictionary<uint, uint>()
{
{ 10, 0x4 }, { 11, 0x5 }, { 12, 0x7 }
};
private static readonly Dictionary<uint, uint> BlackLen8TermCodes = new Dictionary<uint, uint>()
{
{ 13, 0x4 }, { 14, 0x7 }
};
private static readonly Dictionary<uint, uint> BlackLen9TermCodes = new Dictionary<uint, uint>()
{
{ 15, 0x18 }
};
private static readonly Dictionary<uint, uint> BlackLen10TermCodes = new Dictionary<uint, uint>()
{
{ 0, BlackZeroRunTermCode }, { 16, 0x17 }, { 17, 0x18 }, { 18, 0x8 }
};
private static readonly Dictionary<uint, uint> BlackLen11TermCodes = new Dictionary<uint, uint>()
{
{ 19, 0x67 }, { 20, 0x68 }, { 21, 0x6C }, { 22, 0x37 }, { 23, 0x28 }, { 24, 0x17 }, { 25, 0x18 }
};
private static readonly Dictionary<uint, uint> BlackLen12TermCodes = new Dictionary<uint, uint>()
{
{ 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<uint, uint> WhiteLen5MakeupCodes = new Dictionary<uint, uint>()
{
{ 64, 0x1B }, { 128, 0x12 }
};
private static readonly Dictionary<uint, uint> WhiteLen6MakeupCodes = new Dictionary<uint, uint>()
{
{ 192, 0x17 }, { 1664, 0x18 }
};
private static readonly Dictionary<uint, uint> WhiteLen8MakeupCodes = new Dictionary<uint, uint>()
{
{ 320, 0x36 }, { 384, 0x37 }, { 448, 0x64 }, { 512, 0x65 }, { 576, 0x68 }, { 640, 0x67 }
};
private static readonly Dictionary<uint, uint> WhiteLen7MakeupCodes = new Dictionary<uint, uint>()
{
{ 256, 0x37 }
};
private static readonly Dictionary<uint, uint> WhiteLen9MakeupCodes = new Dictionary<uint, uint>()
{
{ 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<uint, uint> WhiteLen11MakeupCodes = new Dictionary<uint, uint>()
{
{ 1792, 0x8 }, { 1856, 0xC }, { 1920, 0xD }
};
private static readonly Dictionary<uint, uint> WhiteLen12MakeupCodes = new Dictionary<uint, uint>()
{
{ 1984, 0x12 }, { 2048, 0x13 }, { 2112, 0x14 }, { 2176, 0x15 }, { 2240, 0x16 }, { 2304, 0x17 }, { 2368, 0x1C },
{ 2432, 0x1D }, { 2496, 0x1E }, { 2560, 0x1F }
};
private static readonly Dictionary<uint, uint> BlackLen10MakeupCodes = new Dictionary<uint, uint>()
{
{ 64, 0xF }
};
private static readonly Dictionary<uint, uint> BlackLen11MakeupCodes = new Dictionary<uint, uint>()
{
{ 1792, 0x8 }, { 1856, 0xC }, { 1920, 0xD }
};
private static readonly Dictionary<uint, uint> BlackLen12MakeupCodes = new Dictionary<uint, uint>()
{
{ 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<uint, uint> BlackLen13MakeupCodes = new Dictionary<uint, uint>()
{
{ 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<byte> compressedDataBuffer;
/// <summary>
/// Initializes a new instance of the <see cref="TiffCcittCompressor" /> class.
/// </summary>
/// <param name="output">The output.</param>
/// <param name="allocator">The allocator.</param>
/// <param name="width">The width.</param>
/// <param name="bitsPerPixel">The bits per pixel.</param>
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;
}
/// <summary>
/// Gets the best makeup run length for a given run length
/// </summary>
/// <param name="runLength">A run length needing a makeup code</param>
/// <returns>The makeup length for <paramref name="runLength"/>.</returns>
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];
}
/// <summary>
/// Gets the terminating code for a run length.
/// </summary>
/// <param name="runLength">The run length to get the terminating code for.</param>
/// <param name="codeLength">The length of the terminating code.</param>
/// <param name="isWhiteRun">If <c>true</c>, the run is of white pixels.
/// If <c>false</c> the run is of black pixels</param>
/// <returns>The terminating code for a run of length <paramref name="runLength"/></returns>
protected uint GetTermCode(uint runLength, out uint codeLength, bool isWhiteRun)
{
if (isWhiteRun)
{
return this.GetWhiteTermCode(runLength, out codeLength);
}
return this.GetBlackTermCode(runLength, out codeLength);
}
/// <summary>
/// Gets the makeup code for a run length.
/// </summary>
/// <param name="runLength">The run length to get the makeup code for.</param>
/// <param name="codeLength">The length of the makeup code.</param>
/// <param name="isWhiteRun">If <c>true</c>, the run is of white pixels.
/// If <c>false</c> the run is of black pixels</param>
/// <returns>The makeup code for a run of length <paramref name="runLength"/></returns>
protected uint GetMakeupCode(uint runLength, out uint codeLength, bool isWhiteRun)
{
if (isWhiteRun)
{
return this.GetWhiteMakeupCode(runLength, out codeLength);
}
return this.GetBlackMakeupCode(runLength, out codeLength);
}
/// <summary>
/// Pads output to the next byte
/// </summary>
/// <remarks>
/// If the output is not currently on a byte boundary,
/// zero-pad it to the next byte
/// </remarks>
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;
}
}
/// <summary>
/// Writes a code to the output.
/// </summary>
/// <param name="codeLength">The length of the code to write.</param>
/// <param name="code">The code to be written.</param>
/// <param name="compressedData">The destination buffer to write the code to.</param>
protected void WriteCode(uint codeLength, uint code, Span<byte> 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--;
}
}
/// <summary>
/// Writes a image compressed with CCITT T6 to the stream.
/// </summary>
/// <param name="pixelsAsGray">The pixels as 8-bit gray array.</param>
/// <param name="height">The strip height.</param>
public override void CompressStrip(Span<byte> 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<byte> 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));
}
/// <summary>
/// Compress a data strip
/// </summary>
/// <param name="pixelsAsGray">The pixels as 8-bit gray array.</param>
/// <param name="height">The strip height.</param>
/// <param name="compressedData">The destination for the compressed data.</param>
protected abstract void CompressStrip(Span<byte> pixelsAsGray, int height, Span<byte> compressedData);
/// <inheritdoc/>
protected override void Dispose(bool disposing) => this.compressedDataBuffer?.Dispose();
/// <inheritdoc/>
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<byte>(maxNeededBytes);
}
}
}

12
src/ImageSharp/Formats/Tiff/Compression/TiffBaseDecompressor.cs

@ -34,17 +34,15 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression
/// <param name="stripByteCount">The number of bytes to read from the input stream.</param> /// <param name="stripByteCount">The number of bytes to read from the input stream.</param>
/// <param name="stripHeight">The height of the strip.</param> /// <param name="stripHeight">The height of the strip.</param>
/// <param name="buffer">The output buffer for uncompressed data.</param> /// <param name="buffer">The output buffer for uncompressed data.</param>
public void Decompress(BufferedReadStream stream, uint stripOffset, uint stripByteCount, int stripHeight, Span<byte> buffer) public void Decompress(BufferedReadStream stream, ulong stripOffset, ulong stripByteCount, int stripHeight, Span<byte> buffer)
{ {
if (stripByteCount > int.MaxValue) DebugGuard.MustBeLessThanOrEqualTo(stripOffset, (ulong)long.MaxValue, nameof(stripOffset));
{ DebugGuard.MustBeLessThanOrEqualTo(stripByteCount, (ulong)long.MaxValue, nameof(stripByteCount));
TiffThrowHelper.ThrowImageFormatException("The StripByteCount value is too big.");
}
stream.Seek(stripOffset, SeekOrigin.Begin); stream.Seek((long)stripOffset, SeekOrigin.Begin);
this.Decompress(stream, (int)stripByteCount, stripHeight, buffer); 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."); TiffThrowHelper.ThrowImageFormatException("Out of range when reading a strip.");
} }

5
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"); DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression");
return new T4BitCompressor(output, allocator, width, bitsPerPixel, false); 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: case TiffCompression.Ccitt1D:
DebugGuard.IsTrue(compressionLevel == DeflateCompressionLevel.DefaultCompression, "No deflate compression level is expected to be set"); 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"); DebugGuard.IsTrue(predictor == TiffPredictor.None, "Predictor should only be used with lzw or deflate compression");

3
src/ImageSharp/Formats/Tiff/Constants/TiffCompression.cs

@ -35,9 +35,6 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Constants
/// <summary> /// <summary>
/// T6-encoding: CCITT T.6 bi-level encoding (see Section 11 of the TIFF 6.0 specification). /// 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.
/// </summary> /// </summary>
CcittGroup4Fax = 4, CcittGroup4Fax = 4,

10
src/ImageSharp/Formats/Tiff/Constants/TiffConstants.cs

@ -35,6 +35,16 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Constants
/// </summary> /// </summary>
public const ushort HeaderMagicNumber = 42; public const ushort HeaderMagicNumber = 42;
/// <summary>
/// The big tiff header magic number
/// </summary>
public const ushort BigTiffHeaderMagicNumber = 43;
/// <summary>
/// The big tiff bytesize of offsets value.
/// </summary>
public const ushort BigTiffBytesize = 8;
/// <summary> /// <summary>
/// RowsPerStrip default value, which is effectively infinity. /// RowsPerStrip default value, which is effectively infinity.
/// </summary> /// </summary>

77
src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Exif;
namespace SixLabors.ImageSharp.Formats.Tiff namespace SixLabors.ImageSharp.Formats.Tiff
@ -14,23 +15,27 @@ namespace SixLabors.ImageSharp.Formats.Tiff
/// </summary> /// </summary>
internal class DirectoryReader 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) private ulong nextIfdOffset;
// todo: different tags can link to the same data (stream offset) - investigate
private readonly SortedList<uint, Action> lazyLoaders = new(new DuplicateKeyComparer<uint>());
public DirectoryReader(Stream stream) => this.stream = stream; public DirectoryReader(Stream stream, MemoryAllocator allocator)
{
this.stream = stream;
this.allocator = allocator;
}
/// <summary> /// <summary>
/// Gets the byte order. /// Gets the byte order.
/// </summary> /// </summary>
public ByteOrder ByteOrder { get; private set; } public ByteOrder ByteOrder { get; private set; }
public bool IsBigTiff { get; private set; }
/// <summary> /// <summary>
/// Reads image file directories. /// Reads image file directories.
/// </summary> /// </summary>
@ -38,14 +43,19 @@ namespace SixLabors.ImageSharp.Formats.Tiff
public IEnumerable<ExifProfile> Read() public IEnumerable<ExifProfile> Read()
{ {
this.ByteOrder = ReadByteOrder(this.stream); this.ByteOrder = ReadByteOrder(this.stream);
this.nextIfdOffset = new HeaderReader(this.stream, this.ByteOrder).ReadFileHeader(); var headerReader = new HeaderReader(this.stream, this.ByteOrder);
return this.ReadIfds(); headerReader.ReadFileHeader();
this.nextIfdOffset = headerReader.FirstIfdOffset;
this.IsBigTiff = headerReader.IsBigTiff;
return this.ReadIfds(headerReader.IsBigTiff);
} }
private static ByteOrder ReadByteOrder(Stream stream) private static ByteOrder ReadByteOrder(Stream stream)
{ {
var headerBytes = new byte[2]; Span<byte> headerBytes = stackalloc byte[2];
stream.Read(headerBytes, 0, 2); stream.Read(headerBytes);
if (headerBytes[0] == TiffConstants.ByteOrderLittleEndian && headerBytes[1] == TiffConstants.ByteOrderLittleEndian) if (headerBytes[0] == TiffConstants.ByteOrderLittleEndian && headerBytes[1] == TiffConstants.ByteOrderLittleEndian)
{ {
return ByteOrder.LittleEndian; return ByteOrder.LittleEndian;
@ -59,16 +69,26 @@ namespace SixLabors.ImageSharp.Formats.Tiff
throw TiffThrowHelper.ThrowInvalidHeader(); throw TiffThrowHelper.ThrowInvalidHeader();
} }
private IEnumerable<ExifProfile> ReadIfds() private IEnumerable<ExifProfile> ReadIfds(bool isBigTiff)
{ {
var readers = new List<EntryReader>(); var readers = new List<EntryReader>();
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); var reader = new EntryReader(this.stream, this.ByteOrder, this.allocator);
reader.ReadTags(); 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); readers.Add(reader);
if (readers.Count >= DirectoryMax) if (readers.Count >= DirectoryMax)
@ -77,36 +97,15 @@ namespace SixLabors.ImageSharp.Formats.Tiff
} }
} }
// Sequential reading big values. var list = new List<ExifProfile>(readers.Count);
foreach (Action loader in this.lazyLoaders.Values)
{
loader();
}
var list = new List<ExifProfile>();
foreach (EntryReader reader in readers) foreach (EntryReader reader in readers)
{ {
reader.ReadBigValues();
var profile = new ExifProfile(reader.Values, reader.InvalidTags); var profile = new ExifProfile(reader.Values, reader.InvalidTags);
list.Add(profile); list.Add(profile);
} }
return list; return list;
} }
/// <summary>
/// <see cref="DuplicateKeyComparer{TKey}"/> used for possibility add a duplicate offsets (but tags don't duplicate).
/// </summary>
/// <typeparam name="TKey">The type of the key.</typeparam>
private class DuplicateKeyComparer<TKey> : IComparer<TKey>
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;
}
}
} }
} }

68
src/ImageSharp/Formats/Tiff/Ifd/EntryReader.cs

@ -1,64 +1,78 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Exif;
namespace SixLabors.ImageSharp.Formats.Tiff namespace SixLabors.ImageSharp.Formats.Tiff
{ {
internal class EntryReader : BaseExifReader internal class EntryReader : BaseExifReader
{ {
private readonly uint startOffset; public EntryReader(Stream stream, ByteOrder byteOrder, MemoryAllocator allocator)
: base(stream, allocator) =>
private readonly SortedList<uint, Action> lazyLoaders;
public EntryReader(Stream stream, ByteOrder byteOrder, uint ifdOffset, SortedList<uint, Action> lazyLoaders)
: base(stream)
{
this.IsBigEndian = byteOrder == ByteOrder.BigEndian; this.IsBigEndian = byteOrder == ByteOrder.BigEndian;
this.startOffset = ifdOffset;
this.lazyLoaders = lazyLoaders;
}
public List<IExifValue> Values { get; } = new(); public List<IExifValue> 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); if (!isBigTiff)
this.NextIfdOffset = this.ReadUInt32(); {
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) => public void ReadBigValues() => this.ReadBigValues(this.Values);
this.lazyLoaders.Add(offset, reader);
} }
internal class HeaderReader : BaseExifReader internal class HeaderReader : BaseExifReader
{ {
public HeaderReader(Stream stream, ByteOrder byteOrder) public HeaderReader(Stream stream, ByteOrder byteOrder)
: base(stream) => : base(stream, null) =>
this.IsBigEndian = byteOrder == ByteOrder.BigEndian; 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(); 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(); ushort bytesize = this.ReadUInt16();
return this.FirstIfdOffset; 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();
}
} }
} }

2
src/ImageSharp/Formats/Tiff/README.md

@ -45,7 +45,7 @@
|Ccitt1D | Y | Y | | |Ccitt1D | Y | Y | |
|PackBits | Y | Y | | |PackBits | Y | Y | |
|CcittGroup3Fax | 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 | |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 | |Old Jpeg | | | We should not even try to support this |
|Jpeg (Technote 2) | Y | Y | | |Jpeg (Technote 2) | Y | Y | |

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

@ -1,6 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers; using System.Buffers;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@ -12,6 +13,7 @@ using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff namespace SixLabors.ImageSharp.Formats.Tiff
@ -41,6 +43,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff
/// </summary> /// </summary>
private ByteOrder byteOrder; private ByteOrder byteOrder;
/// <summary>
/// Indicating whether is BigTiff format.
/// </summary>
private bool isBigTiff;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="TiffDecoderCore" /> class. /// Initializes a new instance of the <see cref="TiffDecoderCore" /> class.
/// </summary> /// </summary>
@ -141,10 +148,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
this.inputStream = stream; this.inputStream = stream;
var reader = new DirectoryReader(stream); var reader = new DirectoryReader(stream, this.Configuration.MemoryAllocator);
IEnumerable<ExifProfile> directories = reader.Read(); IEnumerable<ExifProfile> directories = reader.Read();
this.byteOrder = reader.ByteOrder; this.byteOrder = reader.ByteOrder;
this.isBigTiff = reader.IsBigTiff;
var frames = new List<ImageFrame<TPixel>>(); var frames = new List<ImageFrame<TPixel>>();
foreach (ExifProfile ifd in directories) foreach (ExifProfile ifd in directories)
@ -154,7 +162,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
frames.Add(frame); 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 // TODO: Tiff frames can have different sizes
ImageFrame<TPixel> root = frames[0]; ImageFrame<TPixel> root = frames[0];
@ -174,13 +182,13 @@ namespace SixLabors.ImageSharp.Formats.Tiff
public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{ {
this.inputStream = stream; this.inputStream = stream;
var reader = new DirectoryReader(stream); var reader = new DirectoryReader(stream, this.Configuration.MemoryAllocator);
IEnumerable<ExifProfile> directories = reader.Read(); IEnumerable<ExifProfile> directories = reader.Read();
ExifProfile rootFrameExifProfile = directories.First(); ExifProfile rootFrameExifProfile = directories.First();
var rootMetadata = TiffFrameMetadata.Parse(rootFrameExifProfile); 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 width = GetImageWidth(rootFrameExifProfile);
int height = GetImageHeight(rootFrameExifProfile); int height = GetImageHeight(rootFrameExifProfile);
@ -197,9 +205,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff
private ImageFrame<TPixel> DecodeFrame<TPixel>(ExifProfile tags, CancellationToken cancellationToken) private ImageFrame<TPixel> DecodeFrame<TPixel>(ExifProfile tags, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
ImageFrameMetadata imageFrameMetaData = this.ignoreMetadata ? var imageFrameMetaData = new ImageFrameMetadata();
new ImageFrameMetadata() : if (!this.ignoreMetadata)
new ImageFrameMetadata { ExifProfile = tags, XmpProfile = tags.GetValue(ExifTag.XMP)?.Value }; {
imageFrameMetaData.ExifProfile = tags;
}
TiffFrameMetadata tiffFrameMetaData = imageFrameMetaData.GetTiffMetadata(); TiffFrameMetadata tiffFrameMetaData = imageFrameMetaData.GetTiffMetadata();
TiffFrameMetadata.Parse(tiffFrameMetaData, tags); TiffFrameMetadata.Parse(tiffFrameMetaData, tags);
@ -211,21 +221,58 @@ namespace SixLabors.ImageSharp.Formats.Tiff
var frame = new ImageFrame<TPixel>(this.Configuration, width, height, imageFrameMetaData); var frame = new ImageFrame<TPixel>(this.Configuration, width, height, imageFrameMetaData);
int rowsPerStrip = tags.GetValue(ExifTag.RowsPerStrip) != null ? (int)tags.GetValue(ExifTag.RowsPerStrip).Value : TiffConstants.RowsPerStripInfinity; 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<ulong> stripOffsetsMemory = this.ConvertNumbers(stripOffsetsArray, out Span<ulong> stripOffsets);
IMemoryOwner<ulong> stripByteCountsMemory = this.ConvertNumbers(stripByteCountsArray, out Span<ulong> stripByteCounts);
if (this.PlanarConfiguration == TiffPlanarConfiguration.Planar) if (this.PlanarConfiguration == TiffPlanarConfiguration.Planar)
{ {
this.DecodeStripsPlanar(frame, rowsPerStrip, stripOffsets, stripByteCounts, cancellationToken); this.DecodeStripsPlanar(
frame,
rowsPerStrip,
stripOffsets,
stripByteCounts,
cancellationToken);
} }
else else
{ {
this.DecodeStripsChunky(frame, rowsPerStrip, stripOffsets, stripByteCounts, cancellationToken); this.DecodeStripsChunky(
frame,
rowsPerStrip,
stripOffsets,
stripByteCounts,
cancellationToken);
} }
stripOffsetsMemory?.Dispose();
stripByteCountsMemory?.Dispose();
return frame; return frame;
} }
private IMemoryOwner<ulong> ConvertNumbers(Array array, out Span<ulong> span)
{
if (array is Number[] numbers)
{
IMemoryOwner<ulong> memory = this.memoryAllocator.Allocate<ulong>(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;
}
}
/// <summary> /// <summary>
/// Calculates the size (in bytes) for a pixel buffer using the determined color format. /// Calculates the size (in bytes) for a pixel buffer using the determined color format.
/// </summary> /// </summary>
@ -276,7 +323,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
/// <param name="stripOffsets">An array of byte offsets to each strip in the image.</param> /// <param name="stripOffsets">An array of byte offsets to each strip in the image.</param>
/// <param name="stripByteCounts">An array of the size of each strip (in bytes).</param> /// <param name="stripByteCounts">An array of the size of each strip (in bytes).</param>
/// <param name="cancellationToken">The token to monitor cancellation.</param> /// <param name="cancellationToken">The token to monitor cancellation.</param>
private void DecodeStripsPlanar<TPixel>(ImageFrame<TPixel> frame, int rowsPerStrip, Number[] stripOffsets, Number[] stripByteCounts, CancellationToken cancellationToken) private void DecodeStripsPlanar<TPixel>(ImageFrame<TPixel> frame, int rowsPerStrip, Span<ulong> stripOffsets, Span<ulong> stripByteCounts, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
int stripsPerPixel = this.BitsPerSample.Channels; int stripsPerPixel = this.BitsPerSample.Channels;
@ -329,10 +376,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff
{ {
decompressor.Decompress( decompressor.Decompress(
this.inputStream, this.inputStream,
(uint)stripOffsets[stripIndex], stripOffsets[stripIndex],
(uint)stripByteCounts[stripIndex], stripByteCounts[stripIndex],
stripHeight, stripHeight,
stripBuffers[planeIndex].GetSpan()); stripBuffers[planeIndex].GetSpan());
stripIndex += stripsPerPlane; stripIndex += stripsPerPlane;
} }
@ -357,7 +405,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
/// <param name="stripOffsets">The strip offsets.</param> /// <param name="stripOffsets">The strip offsets.</param>
/// <param name="stripByteCounts">The strip byte counts.</param> /// <param name="stripByteCounts">The strip byte counts.</param>
/// <param name="cancellationToken">The token to monitor cancellation.</param> /// <param name="cancellationToken">The token to monitor cancellation.</param>
private void DecodeStripsChunky<TPixel>(ImageFrame<TPixel> frame, int rowsPerStrip, Number[] stripOffsets, Number[] stripByteCounts, CancellationToken cancellationToken) private void DecodeStripsChunky<TPixel>(ImageFrame<TPixel> frame, int rowsPerStrip, Span<ulong> stripOffsets, Span<ulong> stripByteCounts, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
// If the rowsPerStrip has the default value, which is effectively infinity. That is, the entire image is one strip. // 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; int bitsPerPixel = this.BitsPerPixel;
using IMemoryOwner<byte> stripBuffer = this.memoryAllocator.Allocate<byte>(uncompressedStripSize, AllocationOptions.Clean); using IMemoryOwner<byte> stripBuffer = this.memoryAllocator.Allocate<byte>(uncompressedStripSize, AllocationOptions.Clean);
System.Span<byte> stripBufferSpan = stripBuffer.GetSpan(); Span<byte> stripBufferSpan = stripBuffer.GetSpan();
Buffer2D<TPixel> pixels = frame.PixelBuffer; Buffer2D<TPixel> pixels = frame.PixelBuffer;
using TiffBaseDecompressor decompressor = TiffDecompressorsFactory.Create( using TiffBaseDecompressor decompressor = TiffDecompressorsFactory.Create(
@ -413,7 +461,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff
break; 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); 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"); 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; return (int)width.Value;
} }

20
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.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc; using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff namespace SixLabors.ImageSharp.Formats.Tiff
@ -18,7 +19,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
/// </summary> /// </summary>
internal static class TiffDecoderMetadataCreator internal static class TiffDecoderMetadataCreator
{ {
public static ImageMetadata Create<TPixel>(List<ImageFrame<TPixel>> frames, bool ignoreMetadata, ByteOrder byteOrder) public static ImageMetadata Create<TPixel>(List<ImageFrame<TPixel>> frames, bool ignoreMetadata, ByteOrder byteOrder, bool isBigTiff)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
if (frames.Count < 1) if (frames.Count < 1)
@ -26,13 +27,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
TiffThrowHelper.ThrowImageFormatException("Expected at least one frame."); TiffThrowHelper.ThrowImageFormatException("Expected at least one frame.");
} }
var imageMetaData = new ImageMetadata(); ImageMetadata imageMetaData = Create(byteOrder, isBigTiff, frames[0].Metadata.ExifProfile);
ExifProfile exifProfileRootFrame = frames[0].Metadata.ExifProfile;
SetResolution(imageMetaData, exifProfileRootFrame);
TiffMetadata tiffMetadata = imageMetaData.GetTiffMetadata();
tiffMetadata.ByteOrder = byteOrder;
if (!ignoreMetadata) if (!ignoreMetadata)
{ {
@ -45,6 +40,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff
frameMetaData.IptcProfile = new IptcProfile(iptcBytes); frameMetaData.IptcProfile = new IptcProfile(iptcBytes);
} }
IExifValue<byte[]> xmpProfileBytes = frameMetaData.ExifProfile.GetValue(ExifTag.XMP);
if (xmpProfileBytes != null)
{
frameMetaData.XmpProfile = new XmpProfile(xmpProfileBytes.Value);
}
IExifValue<byte[]> iccProfileBytes = frameMetaData.ExifProfile.GetValue(ExifTag.IccProfile); IExifValue<byte[]> iccProfileBytes = frameMetaData.ExifProfile.GetValue(ExifTag.IccProfile);
if (iccProfileBytes != null) if (iccProfileBytes != null)
{ {
@ -56,13 +57,14 @@ namespace SixLabors.ImageSharp.Formats.Tiff
return imageMetaData; return imageMetaData;
} }
public static ImageMetadata Create(ByteOrder byteOrder, ExifProfile exifProfile) public static ImageMetadata Create(ByteOrder byteOrder, bool isBigTiff, ExifProfile exifProfile)
{ {
var imageMetaData = new ImageMetadata(); var imageMetaData = new ImageMetadata();
SetResolution(imageMetaData, exifProfile); SetResolution(imageMetaData, exifProfile);
TiffMetadata tiffMetadata = imageMetaData.GetTiffMetadata(); TiffMetadata tiffMetadata = imageMetaData.GetTiffMetadata();
tiffMetadata.ByteOrder = byteOrder; tiffMetadata.ByteOrder = byteOrder;
tiffMetadata.FormatType = isBigTiff ? TiffFormatType.BigTIFF : TiffFormatType.Default;
return imageMetaData; return imageMetaData;
} }

13
src/ImageSharp/Formats/Tiff/TiffDecoderOptionsParser.cs

@ -24,12 +24,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff
/// <param name="frameMetadata">The IFD entries container to read the image format information for current frame.</param> /// <param name="frameMetadata">The IFD entries container to read the image format information for current frame.</param>
public static void VerifyAndParse(this TiffDecoderCore options, ExifProfile exifProfile, TiffFrameMetadata frameMetadata) 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."); 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."); TiffThrowHelper.ThrowNotSupported("ExtraSamples is not supported.");
} }
@ -95,12 +95,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff
private static void VerifyRequiredFieldsArePresent(ExifProfile exifProfile, TiffFrameMetadata frameMetadata) 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!"); 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!"); 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) 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: case TiffCompression.None:
{ {
@ -441,7 +442,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
default: default:
{ {
TiffThrowHelper.ThrowNotSupported($"The specified TIFF compression format {compression} is not supported"); TiffThrowHelper.ThrowNotSupported($"The specified TIFF compression format '{compression}' is not supported");
break; break;
} }
} }

12
src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs

@ -6,6 +6,7 @@ using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
namespace SixLabors.ImageSharp.Formats.Tiff namespace SixLabors.ImageSharp.Formats.Tiff
{ {
@ -57,9 +58,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff
{ {
ImageFrame rootFrame = image.Frames.RootFrame; ImageFrame rootFrame = image.Frames.RootFrame;
ExifProfile rootFrameExifProfile = rootFrame.Metadata.ExifProfile ?? new ExifProfile(); 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); this.ProcessMetadata(rootFrameExifProfile);
if (!this.Collector.Entries.Exists(t => t.Tag == ExifTag.Software)) 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) if (exifProfile != null && exifProfile.Parts != ExifParts.None)
{ {
@ -203,7 +204,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
{ {
var xmp = new ExifByteArray(ExifTagValue.XMP, ExifDataType.Byte) var xmp = new ExifByteArray(ExifTagValue.XMP, ExifDataType.Byte)
{ {
Value = xmpProfile Value = xmpProfile.Data
}; };
this.Collector.Add(xmp); this.Collector.Add(xmp);
@ -394,6 +395,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff
case TiffCompression.CcittGroup3Fax: case TiffCompression.CcittGroup3Fax:
return (ushort)TiffCompression.CcittGroup3Fax; return (ushort)TiffCompression.CcittGroup3Fax;
case TiffCompression.CcittGroup4Fax:
return (ushort)TiffCompression.CcittGroup4Fax;
case TiffCompression.Ccitt1D: case TiffCompression.Ccitt1D:
return (ushort)TiffCompression.Ccitt1D; return (ushort)TiffCompression.Ccitt1D;

21
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
{
/// <summary>
/// The TIFF format type enum.
/// </summary>
public enum TiffFormatType
{
/// <summary>
/// The TIFF file format type.
/// </summary>
Default,
/// <summary>
/// The BigTIFF format type.
/// </summary>
BigTIFF
}
}

41
src/ImageSharp/Formats/Tiff/TiffImageFormatDetector.cs

@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
public sealed class TiffImageFormatDetector : IImageFormatDetector public sealed class TiffImageFormatDetector : IImageFormatDetector
{ {
/// <inheritdoc/> /// <inheritdoc/>
public int HeaderSize => 4; public int HeaderSize => 8;
/// <inheritdoc/> /// <inheritdoc/>
public IImageFormat DetectFormat(ReadOnlySpan<byte> header) public IImageFormat DetectFormat(ReadOnlySpan<byte> header)
@ -26,9 +26,42 @@ namespace SixLabors.ImageSharp.Formats.Tiff
private bool IsSupportedFileFormat(ReadOnlySpan<byte> header) private bool IsSupportedFileFormat(ReadOnlySpan<byte> header)
{ {
return header.Length >= this.HeaderSize && if (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[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;
} }
} }
} }

5
src/ImageSharp/Formats/Tiff/TiffMetadata.cs

@ -26,6 +26,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff
/// </summary> /// </summary>
public ByteOrder ByteOrder { get; set; } public ByteOrder ByteOrder { get; set; }
/// <summary>
/// Gets or sets the format type.
/// </summary>
public TiffFormatType FormatType { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
public IDeepCloneable DeepClone() => new TiffMetadata(this); public IDeepCloneable DeepClone() => new TiffMetadata(this);
} }

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

@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
/// </summary> /// </summary>
/// <param name="errorMessage">The error message for the exception.</param> /// <param name="errorMessage">The error message for the exception.</param>
[MethodImpl(MethodImplOptions.NoInlining)] [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)] [MethodImpl(InliningOptions.ColdPath)]
public static Exception NotSupportedDecompressor(string compressionType) => throw new NotSupportedException($"Not supported decoder compression method: {compressionType}"); public static Exception NotSupportedDecompressor(string compressionType) => throw new NotSupportedException($"Not supported decoder compression method: {compressionType}");

2
src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs

@ -37,7 +37,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Writers
{ {
int width = this.Image.Width; 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. // Special case for T4BitCompressor.
int stripPixels = width * height; int stripPixels = width * height;

82
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.BitReader;
using SixLabors.ImageSharp.Formats.Webp.Lossless; using SixLabors.ImageSharp.Formats.Webp.Lossless;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
#if SUPPORTS_RUNTIME_INTRINSICS
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
#endif
namespace SixLabors.ImageSharp.Formats.Webp namespace SixLabors.ImageSharp.Formats.Webp
{ {
@ -307,34 +311,94 @@ namespace SixLabors.ImageSharp.Formats.Webp
private static void HorizontalUnfilter(Span<byte> prev, Span<byte> input, Span<byte> dst, int width) private static void HorizontalUnfilter(Span<byte> prev, Span<byte> input, Span<byte> 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<int> last = Vector128<int>.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<byte, long>(ref Unsafe.Add(ref srcRef, i)), 0);
Vector128<byte> a1 = Sse2.Add(a0.AsByte(), last.AsByte());
Vector128<byte> a2 = Sse2.ShiftLeftLogical128BitLane(a1, 1);
Vector128<byte> a3 = Sse2.Add(a1, a2);
Vector128<byte> a4 = Sse2.ShiftLeftLogical128BitLane(a3, 2);
Vector128<byte> a5 = Sse2.Add(a3, a4);
Vector128<byte> a6 = Sse2.ShiftLeftLogical128BitLane(a5, 4);
Vector128<byte> a7 = Sse2.Add(a5, a6);
ref byte outputRef = ref Unsafe.Add(ref MemoryMarshal.GetReference(dst), i);
Unsafe.As<byte, Vector64<byte>>(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]); byte pred = (byte)(prev.IsEmpty ? 0 : prev[0]);
pred = val;
dst[i] = val; for (int i = 0; i < width; i++)
{
byte val = (byte)(pred + input[i]);
pred = val;
dst[i] = val;
}
} }
} }
private static void VerticalUnfilter(Span<byte> prev, Span<byte> input, Span<byte> dst, int width) private static void VerticalUnfilter(Span<byte> prev, Span<byte> input, Span<byte> dst, int width)
{ {
if (prev == null) if (prev.IsEmpty)
{ {
HorizontalUnfilter(null, input, dst, width); HorizontalUnfilter(null, input, dst, width);
} }
else 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<int> a0 = Unsafe.As<byte, Vector256<int>>(ref Unsafe.Add(ref MemoryMarshal.GetReference(input), i));
Vector256<int> b0 = Unsafe.As<byte, Vector256<int>>(ref Unsafe.Add(ref MemoryMarshal.GetReference(prev), i));
Vector256<byte> c0 = Avx2.Add(a0.AsByte(), b0.AsByte());
ref byte outputRef = ref Unsafe.Add(ref MemoryMarshal.GetReference(dst), i);
Unsafe.As<byte, Vector256<byte>>(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<byte> prev, Span<byte> input, Span<byte> dst, int width) private static void GradientUnfilter(Span<byte> prev, Span<byte> input, Span<byte> dst, int width)
{ {
if (prev == null) if (prev.IsEmpty)
{ {
HorizontalUnfilter(null, input, dst, width); HorizontalUnfilter(null, input, dst, width);
} }

133
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
{
/// <summary>
/// Methods for encoding the alpha data of a VP8 image.
/// </summary>
internal class AlphaEncoder : IDisposable
{
private IMemoryOwner<byte> alphaData;
/// <summary>
/// Encodes the alpha channel data.
/// Data is either compressed as lossless webp image or uncompressed.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="ImageFrame{TPixel}"/> to encode from.</param>
/// <param name="configuration">The global configuration.</param>
/// <param name="memoryAllocator">The memory manager.</param>
/// <param name="compress">Indicates, if the data should be compressed with the lossless webp compression.</param>
/// <param name="size">The size in bytes of the alpha data.</param>
/// <returns>The encoded alpha data.</returns>
public IMemoryOwner<byte> EncodeAlpha<TPixel>(Image<TPixel> image, Configuration configuration, MemoryAllocator memoryAllocator, bool compress, out int size)
where TPixel : unmanaged, IPixel<TPixel>
{
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<Rgba32> alphaAsImage = DispatchAlphaToGreen(image, this.alphaData.GetSpan());
size = lossLessEncoder.EncodeAlphaImageData(alphaAsImage, this.alphaData);
return this.alphaData;
}
size = width * height;
return this.alphaData;
}
/// <summary>
/// Store the transparency in the green channel.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="ImageFrame{TPixel}"/> to encode from.</param>
/// <param name="alphaData">A byte sequence of length width * height, containing all the 8-bit transparency values in scan order.</param>
/// <returns>The transparency image.</returns>
private static Image<Rgba32> DispatchAlphaToGreen<TPixel>(Image<TPixel> image, Span<byte> alphaData)
where TPixel : unmanaged, IPixel<TPixel>
{
int width = image.Width;
int height = image.Height;
var alphaAsImage = new Image<Rgba32>(width, height);
for (int y = 0; y < height; y++)
{
Memory<Rgba32> rowBuffer = alphaAsImage.DangerousGetPixelRowMemory(y);
Span<Rgba32> pixelRow = rowBuffer.Span;
Span<byte> 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;
}
/// <summary>
/// Extract the alpha data of the image.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="ImageFrame{TPixel}"/> to encode from.</param>
/// <param name="configuration">The global configuration.</param>
/// <param name="memoryAllocator">The memory manager.</param>
/// <returns>A byte sequence of length width * height, containing all the 8-bit transparency values in scan order.</returns>
private static IMemoryOwner<byte> ExtractAlphaChannel<TPixel>(Image<TPixel> image, Configuration configuration, MemoryAllocator memoryAllocator)
where TPixel : unmanaged, IPixel<TPixel>
{
Buffer2D<TPixel> imageBuffer = image.Frames.RootFrame.PixelBuffer;
int height = image.Height;
int width = image.Width;
IMemoryOwner<byte> alphaDataBuffer = memoryAllocator.Allocate<byte>(width * height);
Span<byte> alphaData = alphaDataBuffer.GetSpan();
using IMemoryOwner<Rgba32> rowBuffer = memoryAllocator.Allocate<Rgba32>(width);
Span<Rgba32> rgbaRow = rowBuffer.GetSpan();
for (int y = 0; y < height; y++)
{
Span<TPixel> rowSpan = imageBuffer.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToRgba32(configuration, rowSpan, rgbaRow);
int offset = y * width;
for (int x = 0; x < width; x++)
{
alphaData[offset + x] = rgbaRow[x].A;
}
}
return alphaDataBuffer;
}
/// <inheritdoc/>
public void Dispose() => this.alphaData?.Dispose();
}
}

89
src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs

@ -5,6 +5,7 @@ using System;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.IO; using System.IO;
using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
namespace SixLabors.ImageSharp.Formats.Webp.BitWriter namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
{ {
@ -46,6 +47,12 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
/// <param name="stream">The stream to write to.</param> /// <param name="stream">The stream to write to.</param>
public void WriteToStream(Stream stream) => stream.Write(this.Buffer.AsSpan(0, this.NumBytes())); public void WriteToStream(Stream stream) => stream.Write(this.Buffer.AsSpan(0, this.NumBytes()));
/// <summary>
/// Writes the encoded bytes of the image to the given buffer. Call Finish() before this.
/// </summary>
/// <param name="dest">The destination buffer.</param>
public void WriteToBuffer(Span<byte> dest) => this.Buffer.AsSpan(0, this.NumBytes()).CopyTo(dest);
/// <summary> /// <summary>
/// Resizes the buffer to write to. /// Resizes the buffer to write to.
/// </summary> /// </summary>
@ -90,34 +97,79 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
} }
/// <summary> /// <summary>
/// Calculates the exif chunk size. /// Calculates the chunk size of EXIF or XMP metadata.
/// </summary>
/// <param name="metadataBytes">The metadata profile bytes.</param>
/// <returns>The metadata chunk size in bytes.</returns>
protected uint MetadataChunkSize(byte[] metadataBytes)
{
uint metaSize = (uint)metadataBytes.Length;
uint metaChunkSize = WebpConstants.ChunkHeaderSize + metaSize + (metaSize & 1);
return metaChunkSize;
}
/// <summary>
/// Calculates the chunk size of a alpha chunk.
/// </summary> /// </summary>
/// <param name="exifBytes">The exif profile bytes.</param> /// <param name="alphaBytes">The alpha chunk bytes.</param>
/// <returns>The exif chunk size in bytes.</returns> /// <returns>The alpha data chunk size in bytes.</returns>
protected uint ExifChunkSize(byte[] exifBytes) protected uint AlphaChunkSize(Span<byte> alphaBytes)
{ {
uint exifSize = (uint)exifBytes.Length; uint alphaSize = (uint)alphaBytes.Length + 1;
uint exifChunkSize = WebpConstants.ChunkHeaderSize + exifSize + (exifSize & 1); uint alphaChunkSize = WebpConstants.ChunkHeaderSize + alphaSize + (alphaSize & 1);
return exifChunkSize; return alphaChunkSize;
} }
/// <summary> /// <summary>
/// Writes the Exif profile to the stream. /// Writes a metadata profile (EXIF or XMP) to the stream.
/// </summary> /// </summary>
/// <param name="stream">The stream to write to.</param> /// <param name="stream">The stream to write to.</param>
/// <param name="exifBytes">The exif profile bytes.</param> /// <param name="metadataBytes">The metadata profile's bytes.</param>
protected void WriteExifProfile(Stream stream, byte[] exifBytes) /// <param name="chunkType">The chuck type to write.</param>
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<byte> 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; /// <summary>
/// Writes the alpha chunk to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="dataBytes">The alpha channel data bytes.</param>
/// <param name="alphaDataIsCompressed">Indicates, if the alpha channel data is compressed.</param>
protected void WriteAlphaChunk(Stream stream, Span<byte> dataBytes, bool alphaDataIsCompressed)
{
uint size = (uint)dataBytes.Length + 1;
Span<byte> buf = this.scratchBuffer.AsSpan(0, 4); Span<byte> buf = this.scratchBuffer.AsSpan(0, 4);
BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Exif); BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Alpha);
stream.Write(buf); stream.Write(buf);
BinaryPrimitives.WriteUInt32LittleEndian(buf, size); BinaryPrimitives.WriteUInt32LittleEndian(buf, size);
stream.Write(buf); stream.Write(buf);
stream.Write(exifBytes);
byte flags = 0;
if (alphaDataIsCompressed)
{
flags |= 1;
}
stream.WriteByte(flags);
stream.Write(dataBytes);
// Add padding byte if needed. // Add padding byte if needed.
if ((size & 1) == 1) if ((size & 1) == 1)
@ -131,10 +183,11 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
/// </summary> /// </summary>
/// <param name="stream">The stream to write to.</param> /// <param name="stream">The stream to write to.</param>
/// <param name="exifProfile">A exif profile or null, if it does not exist.</param> /// <param name="exifProfile">A exif profile or null, if it does not exist.</param>
/// <param name="xmpProfile">A XMP profile or null, if it does not exist.</param>
/// <param name="width">The width of the image.</param> /// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param> /// <param name="height">The height of the image.</param>
/// <param name="hasAlpha">Flag indicating, if a alpha channel is present.</param> /// <param name="hasAlpha">Flag indicating, if a alpha channel is present.</param>
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) if (width > MaxDimension || height > MaxDimension)
{ {
@ -154,6 +207,12 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
flags |= 8; flags |= 8;
} }
if (xmpProfile != null)
{
// Set xmp bit.
flags |= 4;
}
if (hasAlpha) if (hasAlpha)
{ {
// Set alpha bit. // Set alpha bit.

65
src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs

@ -6,6 +6,7 @@ using System.Buffers.Binary;
using System.IO; using System.IO;
using SixLabors.ImageSharp.Formats.Webp.Lossy; using SixLabors.ImageSharp.Formats.Webp.Lossy;
using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
namespace SixLabors.ImageSharp.Formats.Webp.BitWriter namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
{ {
@ -404,20 +405,49 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
/// </summary> /// </summary>
/// <param name="stream">The stream to write to.</param> /// <param name="stream">The stream to write to.</param>
/// <param name="exifProfile">The exif profile.</param> /// <param name="exifProfile">The exif profile.</param>
/// <param name="xmpProfile">The XMP profile.</param>
/// <param name="width">The width of the image.</param> /// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param> /// <param name="height">The height of the image.</param>
/// <param name="hasAlpha">Flag indicating, if a alpha channel is present.</param> /// <param name="hasAlpha">Flag indicating, if a alpha channel is present.</param>
public void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, uint width, uint height, bool hasAlpha) /// <param name="alphaData">The alpha channel data.</param>
/// <param name="alphaDataIsCompressed">Indicates, if the alpha data is compressed.</param>
public void WriteEncodedImageToStream(
Stream stream,
ExifProfile exifProfile,
XmpProfile xmpProfile,
uint width,
uint height,
bool hasAlpha,
Span<byte> alphaData,
bool alphaDataIsCompressed)
{ {
bool isVp8X = false; bool isVp8X = false;
byte[] exifBytes = null; byte[] exifBytes = null;
byte[] xmpBytes = null;
uint riffSize = 0; uint riffSize = 0;
if (exifProfile != null) if (exifProfile != null)
{ {
isVp8X = true; isVp8X = true;
riffSize += ExtendedFileChunkSize;
exifBytes = exifProfile.ToByteArray(); 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(); this.Finish();
@ -440,7 +470,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
riffSize += WebpConstants.TagSize + WebpConstants.ChunkHeaderSize + vp8Size; riffSize += WebpConstants.TagSize + WebpConstants.ChunkHeaderSize + vp8Size;
// Emit headers and partition #0 // 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); bitWriterPartZero.WriteToStream(stream);
// Write the encoded image to the stream. // Write the encoded image to the stream.
@ -452,7 +482,12 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
if (exifProfile != null) 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()); 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<byte> alphaData,
bool alphaDataIsCompressed)
{ {
this.WriteRiffHeader(stream, riffSize); this.WriteRiffHeader(stream, riffSize);
// Write VP8X, header if necessary. // Write VP8X, header if necessary.
if (isVp8X) 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); this.WriteVp8Header(stream, vp8Size);

24
src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs

@ -6,6 +6,7 @@ using System.Buffers.Binary;
using System.IO; using System.IO;
using SixLabors.ImageSharp.Formats.Webp.Lossless; using SixLabors.ImageSharp.Formats.Webp.Lossless;
using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
namespace SixLabors.ImageSharp.Formats.Webp.BitWriter namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
{ {
@ -132,20 +133,30 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
/// </summary> /// </summary>
/// <param name="stream">The stream to write to.</param> /// <param name="stream">The stream to write to.</param>
/// <param name="exifProfile">The exif profile.</param> /// <param name="exifProfile">The exif profile.</param>
/// <param name="xmpProfile">The XMP profile.</param>
/// <param name="width">The width of the image.</param> /// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param> /// <param name="height">The height of the image.</param>
/// <param name="hasAlpha">Flag indicating, if a alpha channel is present.</param> /// <param name="hasAlpha">Flag indicating, if a alpha channel is present.</param>
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; bool isVp8X = false;
byte[] exifBytes = null; byte[] exifBytes = null;
byte[] xmpBytes = null;
uint riffSize = 0; uint riffSize = 0;
if (exifProfile != null) if (exifProfile != null)
{ {
isVp8X = true; isVp8X = true;
riffSize += ExtendedFileChunkSize; riffSize += ExtendedFileChunkSize;
exifBytes = exifProfile.ToByteArray(); 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(); this.Finish();
@ -160,7 +171,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
// Write VP8X, header if necessary. // Write VP8X, header if necessary.
if (isVp8X) 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. // Write magic bytes indicating its a lossless webp.
@ -180,7 +191,12 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
if (exifProfile != null) if (exifProfile != null)
{ {
this.WriteExifProfile(stream, exifBytes); this.WriteMetadataProfile(stream, exifBytes, WebpChunkType.Exif);
}
if (xmpProfile != null)
{
this.WriteMetadataProfile(stream, xmpBytes, WebpChunkType.Xmp);
} }
} }

2
src/ImageSharp/Formats/Webp/IWebpEncoderOptions.cs

@ -10,6 +10,7 @@ namespace SixLabors.ImageSharp.Formats.Webp
{ {
/// <summary> /// <summary>
/// Gets the webp file format used. Either lossless or lossy. /// Gets the webp file format used. Either lossless or lossy.
/// Defaults to lossy.
/// </summary> /// </summary>
WebpFileFormatType? FileFormat { get; } WebpFileFormatType? FileFormat { get; }
@ -30,6 +31,7 @@ namespace SixLabors.ImageSharp.Formats.Webp
/// <summary> /// <summary>
/// Gets a value indicating whether the alpha plane should be compressed with Webp lossless format. /// Gets a value indicating whether the alpha plane should be compressed with Webp lossless format.
/// Defaults to true.
/// </summary> /// </summary>
bool UseAlphaCompression { get; } bool UseAlphaCompression { get; }

1
src/ImageSharp/Formats/Webp/Lossless/LosslessUtils.cs

@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System; using System;
using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;

42
src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs

@ -10,6 +10,7 @@ using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats.Webp.BitWriter; using SixLabors.ImageSharp.Formats.Webp.BitWriter;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Webp.Lossless namespace SixLabors.ImageSharp.Formats.Webp.Lossless
@ -227,7 +228,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless
public Vp8LHashChain HashChain { get; } public Vp8LHashChain HashChain { get; }
/// <summary> /// <summary>
/// Encodes the image to the specified stream from the <see cref="Image{TPixel}"/>. /// Encodes the image as lossless webp to the specified stream.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="Image{TPixel}"/> to encode from.</param> /// <param name="image">The <see cref="Image{TPixel}"/> to encode from.</param>
@ -235,10 +236,12 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless
public void Encode<TPixel>(Image<TPixel> image, Stream stream) public void Encode<TPixel>(Image<TPixel> image, Stream stream)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
image.Metadata.SyncProfiles();
int width = image.Width; int width = image.Width;
int height = image.Height; int height = image.Height;
ImageMetadata metadata = image.Metadata;
metadata.SyncProfiles();
// Convert image pixels to bgra array. // Convert image pixels to bgra array.
bool hasAlpha = this.ConvertPixelsToBgra(image, width, height); bool hasAlpha = this.ConvertPixelsToBgra(image, width, height);
@ -252,7 +255,40 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless
this.EncodeStream(image); this.EncodeStream(image);
// Write bytes from the bitwriter buffer to the stream. // 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);
}
/// <summary>
/// Encodes the alpha image data using the webp lossless compression.
/// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="image">The <see cref="Image{TPixel}"/> to encode from.</param>
/// <param name="alphaData">The destination buffer to write the encoded alpha data to.</param>
/// <returns>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.
/// </returns>
public int EncodeAlphaImageData<TPixel>(Image<TPixel> image, IMemoryOwner<byte> alphaData)
where TPixel : unmanaged, IPixel<TPixel>
{
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;
} }
/// <summary> /// <summary>

52
src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs

@ -7,6 +7,7 @@ using System.IO;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Formats.Webp.BitWriter; using SixLabors.ImageSharp.Formats.Webp.BitWriter;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Webp.Lossy namespace SixLabors.ImageSharp.Formats.Webp.Lossy
@ -70,12 +71,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy
/// </summary> /// </summary>
private int uvAlpha; private int uvAlpha;
/// <summary> private readonly bool alphaCompression;
/// Scratch buffer to reduce allocations.
/// </summary>
private readonly int[] scratch = new int[16];
private readonly byte[] averageBytesPerMb = { 50, 24, 16, 9, 7, 5, 3, 2 };
private const int NumMbSegments = 4; private const int NumMbSegments = 4;
@ -104,6 +100,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy
/// <param name="entropyPasses">Number of entropy-analysis passes (in [1..10]).</param> /// <param name="entropyPasses">Number of entropy-analysis passes (in [1..10]).</param>
/// <param name="filterStrength">The filter the strength of the deblocking filter, between 0 (no filtering) and 100 (maximum filtering).</param> /// <param name="filterStrength">The filter the strength of the deblocking filter, between 0 (no filtering) and 100 (maximum filtering).</param>
/// <param name="spatialNoiseShaping">The spatial noise shaping. 0=off, 100=maximum.</param> /// <param name="spatialNoiseShaping">The spatial noise shaping. 0=off, 100=maximum.</param>
/// <param name="alphaCompression">If true, the alpha channel will be compressed with the lossless compression.</param>
public Vp8Encoder( public Vp8Encoder(
MemoryAllocator memoryAllocator, MemoryAllocator memoryAllocator,
Configuration configuration, Configuration configuration,
@ -113,7 +110,8 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy
WebpEncodingMethod method, WebpEncodingMethod method,
int entropyPasses, int entropyPasses,
int filterStrength, int filterStrength,
int spatialNoiseShaping) int spatialNoiseShaping,
bool alphaCompression)
{ {
this.memoryAllocator = memoryAllocator; this.memoryAllocator = memoryAllocator;
this.configuration = configuration; this.configuration = configuration;
@ -124,6 +122,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy
this.entropyPasses = Numerics.Clamp(entropyPasses, 1, 10); this.entropyPasses = Numerics.Clamp(entropyPasses, 1, 10);
this.filterStrength = Numerics.Clamp(filterStrength, 0, 100); this.filterStrength = Numerics.Clamp(filterStrength, 0, 100);
this.spatialNoiseShaping = Numerics.Clamp(spatialNoiseShaping, 0, 100); this.spatialNoiseShaping = Numerics.Clamp(spatialNoiseShaping, 0, 100);
this.alphaCompression = alphaCompression;
this.rdOptLevel = method is WebpEncodingMethod.BestQuality ? Vp8RdLevel.RdOptTrellisAll this.rdOptLevel = method is WebpEncodingMethod.BestQuality ? Vp8RdLevel.RdOptTrellisAll
: method >= WebpEncodingMethod.Level5 ? Vp8RdLevel.RdOptTrellis : method >= WebpEncodingMethod.Level5 ? Vp8RdLevel.RdOptTrellis
: method >= WebpEncodingMethod.Level3 ? Vp8RdLevel.RdOptBasic : method >= WebpEncodingMethod.Level3 ? Vp8RdLevel.RdOptBasic
@ -173,6 +172,9 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy
this.ResetBoundaryPredictions(); this.ResetBoundaryPredictions();
} }
// This uses C#'s optimization to refer to the static data segment of the assembly, no allocation occurs.
private static ReadOnlySpan<byte> AverageBytesPerMb => new byte[] { 50, 24, 16, 9, 7, 5, 3, 2 };
public int BaseQuant { get; set; } public int BaseQuant { get; set; }
/// <summary> /// <summary>
@ -296,10 +298,11 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy
{ {
int width = image.Width; int width = image.Width;
int height = image.Height; int height = image.Height;
int pixelCount = width * height;
Span<byte> y = this.Y.GetSpan(); Span<byte> y = this.Y.GetSpan();
Span<byte> u = this.U.GetSpan(); Span<byte> u = this.U.GetSpan();
Span<byte> v = this.V.GetSpan(); Span<byte> 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 yStride = width;
int uvStride = (yStride + 1) >> 1; int uvStride = (yStride + 1) >> 1;
@ -317,12 +320,26 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy
this.SetLoopParams(this.quality); this.SetLoopParams(this.quality);
// Initialize the bitwriter. // Initialize the bitwriter.
int averageBytesPerMacroBlock = this.averageBytesPerMb[this.BaseQuant >> 4]; int averageBytesPerMacroBlock = AverageBytesPerMb[this.BaseQuant >> 4];
int expectedSize = this.Mbw * this.Mbh * averageBytesPerMacroBlock; int expectedSize = this.Mbw * this.Mbh * averageBytesPerMacroBlock;
this.bitWriter = new Vp8BitWriter(expectedSize, this); this.bitWriter = new Vp8BitWriter(expectedSize, this);
// TODO: EncodeAlpha(); // Extract and encode alpha channel data, if present.
bool hasAlpha = false; int alphaDataSize = 0;
bool alphaCompressionSucceeded = false;
using var alphaEncoder = new AlphaEncoder();
Span<byte> alphaData = Span<byte>.Empty;
if (hasAlpha)
{
// TODO: This can potentially run in an separate task.
IMemoryOwner<byte> 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. // Stats-collection loop.
this.StatLoop(width, height, yStride, uvStride); this.StatLoop(width, height, yStride, uvStride);
@ -355,8 +372,17 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy
this.AdjustFilterStrength(); this.AdjustFilterStrength();
// Write bytes from the bitwriter buffer to the stream. // Write bytes from the bitwriter buffer to the stream.
image.Metadata.SyncProfiles(); ImageMetadata metadata = image.Metadata;
this.bitWriter.WriteEncodedImageToStream(stream, image.Metadata.ExifProfile, (uint)width, (uint)height, hasAlpha); metadata.SyncProfiles();
this.bitWriter.WriteEncodedImageToStream(
stream,
metadata.ExifProfile,
metadata.XmpProfile,
(uint)width,
(uint)height,
hasAlpha,
alphaData,
this.alphaCompression && alphaCompressionSucceeded);
} }
/// <inheritdoc/> /// <inheritdoc/>

11
src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs

@ -318,7 +318,8 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy
/// <param name="y">Span to store the luma component of the image.</param> /// <param name="y">Span to store the luma component of the image.</param>
/// <param name="u">Span to store the u component of the image.</param> /// <param name="u">Span to store the u component of the image.</param>
/// <param name="v">Span to store the v component of the image.</param> /// <param name="v">Span to store the v component of the image.</param>
public static void ConvertRgbToYuv<TPixel>(Image<TPixel> image, Configuration configuration, MemoryAllocator memoryAllocator, Span<byte> y, Span<byte> u, Span<byte> v) /// <returns>true, if the image contains alpha data.</returns>
public static bool ConvertRgbToYuv<TPixel>(Image<TPixel> image, Configuration configuration, MemoryAllocator memoryAllocator, Span<byte> y, Span<byte> u, Span<byte> v)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
Buffer2D<TPixel> imageBuffer = image.Frames.RootFrame.PixelBuffer; Buffer2D<TPixel> imageBuffer = image.Frames.RootFrame.PixelBuffer;
@ -335,6 +336,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy
Span<Bgra32> bgraRow1 = bgraRow1Buffer.GetSpan(); Span<Bgra32> bgraRow1 = bgraRow1Buffer.GetSpan();
int uvRowIndex = 0; int uvRowIndex = 0;
int rowIndex; int rowIndex;
bool hasAlpha = false;
for (rowIndex = 0; rowIndex < height - 1; rowIndex += 2) for (rowIndex = 0; rowIndex < height - 1; rowIndex += 2)
{ {
Span<TPixel> rowSpan = imageBuffer.DangerousGetRowSpan(rowIndex); Span<TPixel> rowSpan = imageBuffer.DangerousGetRowSpan(rowIndex);
@ -343,6 +345,10 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy
PixelOperations<TPixel>.Instance.ToBgra32(configuration, nextRowSpan, bgraRow1); PixelOperations<TPixel>.Instance.ToBgra32(configuration, nextRowSpan, bgraRow1);
bool rowsHaveAlpha = WebpCommonUtils.CheckNonOpaque(bgraRow0) && WebpCommonUtils.CheckNonOpaque(bgraRow1); bool rowsHaveAlpha = WebpCommonUtils.CheckNonOpaque(bgraRow0) && WebpCommonUtils.CheckNonOpaque(bgraRow1);
if (rowsHaveAlpha)
{
hasAlpha = true;
}
// Downsample U/V planes, two rows at a time. // Downsample U/V planes, two rows at a time.
if (!rowsHaveAlpha) if (!rowsHaveAlpha)
@ -375,10 +381,13 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy
else else
{ {
AccumulateRgba(bgraRow0, bgraRow0, tmpRgbSpan, width); AccumulateRgba(bgraRow0, bgraRow0, tmpRgbSpan, width);
hasAlpha = true;
} }
ConvertRgbaToUv(tmpRgbSpan, u.Slice(uvRowIndex * uvWidth), v.Slice(uvRowIndex * uvWidth), uvWidth); ConvertRgbaToUv(tmpRgbSpan, u.Slice(uvRowIndex * uvWidth), v.Slice(uvRowIndex * uvWidth), uvWidth);
} }
return hasAlpha;
} }
/// <summary> /// <summary>

78
src/ImageSharp/Formats/Webp/WebpDecoderCore.cs

@ -13,6 +13,7 @@ using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Webp 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: /// Reads an the extended webp file header. An extended file header consists of:
/// - A 'VP8X' chunk with information about features used in the file. /// - A 'VP8X' chunk with information about features used in the file.
/// - An optional 'ICCP' chunk with color profile. /// - An optional 'ICCP' chunk with color profile.
/// - An optional 'XMP' chunk with metadata.
/// - An optional 'ANIM' chunk with animation control data. /// - An optional 'ANIM' chunk with animation control data.
/// - An optional 'ALPH' chunk with alpha channel 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. /// 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; this.buffer[3] = 0;
uint height = (uint)BinaryPrimitives.ReadInt32LittleEndian(this.buffer) + 1; uint height = (uint)BinaryPrimitives.ReadInt32LittleEndian(this.buffer) + 1;
// Optional chunks ICCP, ALPH and ANIM can follow here. // Read all the chunks in the order they occur.
WebpChunkType chunkType = this.ReadChunkType(); var info = new WebpImageInfo();
while (IsOptionalVp8XChunk(chunkType)) while (this.currentStream.Position < this.currentStream.Length)
{ {
this.ParseOptionalExtendedChunks(chunkType, features); WebpChunkType chunkType = this.ReadChunkType();
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) if (features.Animation)
@ -242,17 +259,7 @@ namespace SixLabors.ImageSharp.Formats.Webp
return new WebpImageInfo() { Width = width, Height = height, Features = features }; return new WebpImageInfo() { Width = width, Height = height, Features = features };
} }
switch (chunkType) return info;
{
case WebpChunkType.Vp8:
return this.ReadVp8Header(features);
case WebpChunkType.Vp8L:
return this.ReadVp8LHeader(features);
}
WebpThrowHelper.ThrowImageFormatException("Unexpected chunk followed VP8X header");
return new WebpImageInfo();
} }
/// <summary> /// <summary>
@ -413,7 +420,7 @@ namespace SixLabors.ImageSharp.Formats.Webp
} }
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
/// <param name="chunkType">The chunk type.</param> /// <param name="chunkType">The chunk type.</param>
/// <param name="features">The webp image features.</param> /// <param name="features">The webp image features.</param>
@ -440,6 +447,38 @@ namespace SixLabors.ImageSharp.Formats.Webp
break; 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: case WebpChunkType.Animation:
// TODO: Decoding animation is not implemented yet. // TODO: Decoding animation is not implemented yet.
break; break;
@ -451,6 +490,9 @@ namespace SixLabors.ImageSharp.Formats.Webp
features.AlphaData = this.memoryAllocator.Allocate<byte>(alphaDataSize); features.AlphaData = this.memoryAllocator.Allocate<byte>(alphaDataSize);
this.currentStream.Read(features.AlphaData.Memory.Span, 0, alphaDataSize); this.currentStream.Read(features.AlphaData.Memory.Span, 0, alphaDataSize);
break; break;
default:
WebpThrowHelper.ThrowImageFormatException("Unexpected chunk followed VP8X header");
break;
} }
} }
@ -530,7 +572,9 @@ namespace SixLabors.ImageSharp.Formats.Webp
{ {
WebpChunkType.Alpha => true, WebpChunkType.Alpha => true,
WebpChunkType.Animation => true, WebpChunkType.Animation => true,
WebpChunkType.Exif => true,
WebpChunkType.Iccp => true, WebpChunkType.Iccp => true,
WebpChunkType.Xmp => true,
_ => false _ => false
}; };
} }

2
src/ImageSharp/Formats/Webp/WebpEncoder.cs

@ -24,7 +24,7 @@ namespace SixLabors.ImageSharp.Formats.Webp
public WebpEncodingMethod Method { get; set; } = WebpEncodingMethod.Default; public WebpEncodingMethod Method { get; set; } = WebpEncodingMethod.Default;
/// <inheritdoc/> /// <inheritdoc/>
public bool UseAlphaCompression { get; set; } public bool UseAlphaCompression { get; set; } = true;
/// <inheritdoc/> /// <inheritdoc/>
public int EntropyPasses { get; set; } = 1; public int EntropyPasses { get; set; } = 1;

30
src/ImageSharp/Formats/Webp/WebpEncoderCore.cs

@ -22,8 +22,8 @@ namespace SixLabors.ImageSharp.Formats.Webp
private readonly MemoryAllocator memoryAllocator; private readonly MemoryAllocator memoryAllocator;
/// <summary> /// <summary>
/// TODO: not used at the moment.
/// Indicating whether the alpha plane should be compressed with Webp lossless format. /// Indicating whether the alpha plane should be compressed with Webp lossless format.
/// Defaults to true.
/// </summary> /// </summary>
private readonly bool alphaCompression; private readonly bool alphaCompression;
@ -70,6 +70,7 @@ namespace SixLabors.ImageSharp.Formats.Webp
/// <summary> /// <summary>
/// Indicating what file format compression should be used. /// Indicating what file format compression should be used.
/// Defaults to lossy.
/// </summary> /// </summary>
private readonly WebpFileFormatType? fileFormat; private readonly WebpFileFormatType? fileFormat;
@ -99,7 +100,7 @@ namespace SixLabors.ImageSharp.Formats.Webp
} }
/// <summary> /// <summary>
/// Encodes the image to the specified stream from the <see cref="ImageFrame{TPixel}"/>. /// Encodes the image as webp to the specified stream.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="ImageFrame{TPixel}"/> to encode from.</param> /// <param name="image">The <see cref="ImageFrame{TPixel}"/> to encode from.</param>
@ -112,43 +113,44 @@ namespace SixLabors.ImageSharp.Formats.Webp
Guard.NotNull(stream, nameof(stream)); Guard.NotNull(stream, nameof(stream));
this.configuration = image.GetConfiguration(); this.configuration = image.GetConfiguration();
bool lossy; bool lossless;
if (this.fileFormat is not null) if (this.fileFormat is not null)
{ {
lossy = this.fileFormat == WebpFileFormatType.Lossy; lossless = this.fileFormat == WebpFileFormatType.Lossless;
} }
else else
{ {
WebpMetadata webpMetadata = image.Metadata.GetWebpMetadata(); 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.memoryAllocator,
this.configuration, this.configuration,
image.Width, image.Width,
image.Height, image.Height,
this.quality, this.quality,
this.method, this.method,
this.entropyPasses, this.transparentColorMode,
this.filterStrength, this.nearLossless,
this.spatialNoiseShaping); this.nearLosslessQuality);
enc.Encode(image, stream); enc.Encode(image, stream);
} }
else else
{ {
using var enc = new Vp8LEncoder( using var enc = new Vp8Encoder(
this.memoryAllocator, this.memoryAllocator,
this.configuration, this.configuration,
image.Width, image.Width,
image.Height, image.Height,
this.quality, this.quality,
this.method, this.method,
this.transparentColorMode, this.entropyPasses,
this.nearLossless, this.filterStrength,
this.nearLosslessQuality); this.spatialNoiseShaping,
this.alphaCompression);
enc.Encode(image, stream); enc.Encode(image, stream);
} }
} }

1
src/ImageSharp/IO/BufferedReadStream.cs

@ -87,7 +87,6 @@ namespace SixLabors.ImageSharp.IO
set set
{ {
Guard.MustBeGreaterThanOrEqualTo(value, 0, nameof(this.Position)); 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 // Only reset readBufferIndex if we are out of bounds of our working buffer
// otherwise we should simply move the value by the diff. // otherwise we should simply move the value by the diff.

1
src/ImageSharp/IO/ChunkedMemoryStream.cs

@ -168,7 +168,6 @@ namespace SixLabors.ImageSharp.IO
// Position is out of range // Position is out of range
this.readChunk = backupReadChunk; this.readChunk = backupReadChunk;
this.readOffset = backupReadOffset; this.readOffset = backupReadOffset;
ThrowArgumentOutOfRange(nameof(value));
} }
} }
} }

6
src/ImageSharp/Image.Decode.cs

@ -35,8 +35,10 @@ namespace SixLabors.ImageSharp
ImageMetadata metadata) ImageMetadata metadata)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
Buffer2D<TPixel> uninitializedMemoryBuffer = Buffer2D<TPixel> uninitializedMemoryBuffer = configuration.MemoryAllocator.Allocate2D<TPixel>(
configuration.MemoryAllocator.Allocate2D<TPixel>(width, height); width,
height,
configuration.PreferContiguousImageBuffers);
return new Image<TPixel>(configuration, uninitializedMemoryBuffer.FastMemoryGroup, width, height, metadata); return new Image<TPixel>(configuration, uninitializedMemoryBuffer.FastMemoryGroup, width, height, metadata);
} }

10
src/ImageSharp/ImageSharp.csproj

@ -10,7 +10,7 @@
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression> <PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
<RepositoryUrl Condition="'$(RepositoryUrl)' == ''">https://github.com/SixLabors/ImageSharp/</RepositoryUrl> <RepositoryUrl Condition="'$(RepositoryUrl)' == ''">https://github.com/SixLabors/ImageSharp/</RepositoryUrl>
<PackageProjectUrl>$(RepositoryUrl)</PackageProjectUrl> <PackageProjectUrl>$(RepositoryUrl)</PackageProjectUrl>
<PackageTags>Image Resize Crop Gif Jpg Jpeg Bitmap Pbm Png Tga NetCore</PackageTags> <PackageTags>Image Resize Crop Gif Jpg Jpeg Bitmap Pbm Png Tga Tiff WebP NetCore</PackageTags>
<Description>A new, fully featured, fully managed, cross-platform, 2D graphics API for .NET</Description> <Description>A new, fully featured, fully managed, cross-platform, 2D graphics API for .NET</Description>
<Configurations>Debug;Release;Debug-InnerLoop;Release-InnerLoop</Configurations> <Configurations>Debug;Release;Debug-InnerLoop;Release-InnerLoop</Configurations>
</PropertyGroup> </PropertyGroup>
@ -49,6 +49,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="5.0.0" /> <PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="5.0.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="5.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition=" $(TargetFramework.StartsWith('netstandard')) OR '$(TargetFramework)' == 'net472'"> <ItemGroup Condition=" $(TargetFramework.StartsWith('netstandard')) OR '$(TargetFramework)' == 'net472'">
@ -57,13 +58,6 @@
<PackageReference Include="System.Memory" Version="4.5.4" /> <PackageReference Include="System.Memory" Version="4.5.4" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard1.3'">
<PackageReference Include="System.IO.Compression" Version="4.3.0" />
<PackageReference Include="System.IO.UnmanagedMemoryStream" Version="4.3.0" />
<PackageReference Include="System.Threading.Tasks.Parallel" Version="4.3.0" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Update="Formats\Jpeg\Components\Block8x8F.Generated.cs"> <Compile Update="Formats\Jpeg\Components\Block8x8F.Generated.cs">
<DesignTime>True</DesignTime> <DesignTime>True</DesignTime>

56
src/ImageSharp/Memory/Allocators/Internals/RefCountedLifetimeGuard.cs

@ -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
{
/// <summary>
/// Implements reference counting lifetime guard mechanism similar to the one provided by <see cref="SafeHandle"/>,
/// but without the restriction of the guarded object being a handle.
/// </summary>
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();
}
}

79
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
{
/// <summary>
/// Implements reference counting lifetime guard mechanism for memory resources
/// and maintains the value of <see cref="MemoryDiagnostics.TotalUndisposedAllocationCount"/>.
/// </summary>
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();
}
}
}
}
}

2
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; private byte[] array;

4
src/ImageSharp/Memory/Allocators/Internals/UniformUnmanagedMemoryPool.LifetimeGuards.cs

@ -20,9 +20,9 @@ namespace SixLabors.ImageSharp.Memory.Internals
return buffer; 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 UniformUnmanagedMemoryPool pool;
private readonly UnmanagedMemoryHandle[] handles; private readonly UnmanagedMemoryHandle[] handles;

2
src/ImageSharp/Memory/Allocators/Internals/UnmanagedBufferLifetimeGuard.cs

@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Memory.Internals
/// <summary> /// <summary>
/// Defines a strategy for managing unmanaged memory ownership. /// Defines a strategy for managing unmanaged memory ownership.
/// </summary> /// </summary>
internal abstract class UnmanagedBufferLifetimeGuard : RefCountedLifetimeGuard internal abstract class UnmanagedBufferLifetimeGuard : RefCountedMemoryLifetimeGuard
{ {
private UnmanagedMemoryHandle handle; private UnmanagedMemoryHandle handle;

4
src/ImageSharp/Memory/Buffer2DRegion{T}.cs

@ -88,7 +88,7 @@ namespace SixLabors.ImageSharp.Memory
/// <param name="y">The row index</param> /// <param name="y">The row index</param>
/// <returns>The span</returns> /// <returns>The span</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public Span<T> GetRowSpan(int y) public Span<T> DangerousGetRowSpan(int y)
{ {
int yy = this.Rectangle.Y + y; int yy = this.Rectangle.Y + y;
int xx = this.Rectangle.X; int xx = this.Rectangle.X;
@ -152,7 +152,7 @@ namespace SixLabors.ImageSharp.Memory
for (int y = 0; y < this.Rectangle.Height; y++) for (int y = 0; y < this.Rectangle.Height; y++)
{ {
Span<T> row = this.GetRowSpan(y); Span<T> row = this.DangerousGetRowSpan(y);
row.Clear(); row.Clear();
} }
} }

2
src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs

@ -18,7 +18,7 @@ namespace SixLabors.ImageSharp.Memory
public sealed class Owned : MemoryGroup<T>, IEnumerable<Memory<T>> public sealed class Owned : MemoryGroup<T>, IEnumerable<Memory<T>>
{ {
private IMemoryOwner<T>[] memoryOwners; private IMemoryOwner<T>[] memoryOwners;
private RefCountedLifetimeGuard groupLifetimeGuard; private RefCountedMemoryLifetimeGuard groupLifetimeGuard;
public Owned(IMemoryOwner<T>[] memoryOwners, int bufferLength, long totalLength, bool swappable) public Owned(IMemoryOwner<T>[] memoryOwners, int bufferLength, long totalLength, bool swappable)
: base(bufferLength, totalLength) : base(bufferLength, totalLength)

7
src/ImageSharp/Metadata/ImageFrameMetadata.cs

@ -1,12 +1,12 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc; using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
namespace SixLabors.ImageSharp.Metadata namespace SixLabors.ImageSharp.Metadata
{ {
@ -43,8 +43,7 @@ namespace SixLabors.ImageSharp.Metadata
this.ExifProfile = other.ExifProfile?.DeepClone(); this.ExifProfile = other.ExifProfile?.DeepClone();
this.IccProfile = other.IccProfile?.DeepClone(); this.IccProfile = other.IccProfile?.DeepClone();
this.IptcProfile = other.IptcProfile?.DeepClone(); this.IptcProfile = other.IptcProfile?.DeepClone();
this.XmpProfile = other.XmpProfile != null ? new byte[other.XmpProfile.Length] : null; this.XmpProfile = other.XmpProfile?.DeepClone();
other.XmpProfile?.AsSpan().CopyTo(this.XmpProfile.AsSpan());
} }
/// <summary> /// <summary>
@ -55,7 +54,7 @@ namespace SixLabors.ImageSharp.Metadata
/// <summary> /// <summary>
/// Gets or sets the XMP profile. /// Gets or sets the XMP profile.
/// </summary> /// </summary>
internal byte[] XmpProfile { get; set; } public XmpProfile XmpProfile { get; set; }
/// <summary> /// <summary>
/// Gets or sets the list of ICC profiles. /// Gets or sets the list of ICC profiles.

40
src/ImageSharp/Metadata/ImageMetadata.cs

@ -6,6 +6,7 @@ using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc; using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
namespace SixLabors.ImageSharp.Metadata namespace SixLabors.ImageSharp.Metadata
{ {
@ -39,7 +40,7 @@ namespace SixLabors.ImageSharp.Metadata
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ImageMetadata"/> class. /// Initializes a new instance of the <see cref="ImageMetadata"/> class.
/// </summary> /// </summary>
internal ImageMetadata() public ImageMetadata()
{ {
this.horizontalResolution = DefaultHorizontalResolution; this.horizontalResolution = DefaultHorizontalResolution;
this.verticalResolution = DefaultVerticalResolution; this.verticalResolution = DefaultVerticalResolution;
@ -71,7 +72,7 @@ namespace SixLabors.ImageSharp.Metadata
/// <summary> /// <summary>
/// Gets or sets the resolution of the image in x- direction. /// 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 <see cref="ResolutionUnits"/> and should be an positive value.
/// </summary> /// </summary>
/// <value>The density of the image in x- direction.</value> /// <value>The density of the image in x- direction.</value>
public double HorizontalResolution public double HorizontalResolution
@ -89,7 +90,7 @@ namespace SixLabors.ImageSharp.Metadata
/// <summary> /// <summary>
/// Gets or sets the resolution of the image in y- direction. /// 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 <see cref="ResolutionUnits"/> and should be an positive value.
/// </summary> /// </summary>
/// <value>The density of the image in y- direction.</value> /// <value>The density of the image in y- direction.</value>
public double VerticalResolution public double VerticalResolution
@ -107,10 +108,28 @@ namespace SixLabors.ImageSharp.Metadata
/// <summary> /// <summary>
/// Gets or sets unit of measure used when reporting resolution. /// Gets or sets unit of measure used when reporting resolution.
/// 00 : No units; width:height pixel aspect ratio = Ydensity:Xdensity /// <list type="table">
/// 01 : Pixels per inch (2.54 cm) /// <listheader>
/// 02 : Pixels per centimeter /// <term>Value</term>
/// 03 : Pixels per meter /// <description>Unit</description>
/// </listheader>
/// <item>
/// <term>AspectRatio (00)</term>
/// <description>No units; width:height pixel aspect ratio = Ydensity:Xdensity</description>
/// </item>
/// <item>
/// <term>PixelsPerInch (01)</term>
/// <description>Pixels per inch (2.54 cm)</description>
/// </item>
/// <item>
/// <term>PixelsPerCentimeter (02)</term>
/// <description>Pixels per centimeter</description>
/// </item>
/// <item>
/// <term>PixelsPerMeter (03)</term>
/// <description>Pixels per meter (100 cm)</description>
/// </item>
/// </list>
/// </summary> /// </summary>
public PixelResolutionUnit ResolutionUnits { get; set; } public PixelResolutionUnit ResolutionUnits { get; set; }
@ -119,13 +138,18 @@ namespace SixLabors.ImageSharp.Metadata
/// </summary> /// </summary>
public ExifProfile ExifProfile { get; set; } public ExifProfile ExifProfile { get; set; }
/// <summary>
/// Gets or sets the XMP profile.
/// </summary>
public XmpProfile XmpProfile { get; set; }
/// <summary> /// <summary>
/// Gets or sets the list of ICC profiles. /// Gets or sets the list of ICC profiles.
/// </summary> /// </summary>
public IccProfile IccProfile { get; set; } public IccProfile IccProfile { get; set; }
/// <summary> /// <summary>
/// Gets or sets the iptc profile. /// Gets or sets the IPTC profile.
/// </summary> /// </summary>
public IptcProfile IptcProfile { get; set; } public IptcProfile IptcProfile { get; set; }

BIN
src/ImageSharp/Metadata/Profiles/Exif/DC-X008-Translation-2019-E.pdf

Binary file not shown.

4
src/ImageSharp/Metadata/Profiles/Exif/ExifConstants.cs

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System; using System;
using System.Text;
namespace SixLabors.ImageSharp.Metadata.Profiles.Exif namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
{ {
@ -22,5 +23,8 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
0x00, 0x00,
0x2A 0x2A
}; };
// UTF-8 is better than ASCII, UTF-8 encodes the ASCII codes the same way
public static Encoding DefaultEncoding => Encoding.UTF8;
} }
} }

17
src/ImageSharp/Metadata/Profiles/Exif/ExifDataType.cs

@ -80,6 +80,21 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
/// <summary> /// <summary>
/// Reference to an IFD (32-bit (4-byte) unsigned integer). /// Reference to an IFD (32-bit (4-byte) unsigned integer).
/// </summary> /// </summary>
Ifd = 13 Ifd = 13,
/// <summary>
/// A 64-bit (8-byte) unsigned integer.
/// </summary>
Long8 = 16,
/// <summary>
/// A 64-bit (8-byte) signed integer (2's complement notation).
/// </summary>
SignedLong8 = 17,
/// <summary>
/// Reference to an IFD (64-bit (8-byte) unsigned integer).
/// </summary>
Ifd8 = 18,
} }
} }

4
src/ImageSharp/Metadata/Profiles/Exif/ExifDataTypes.cs

@ -32,10 +32,14 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
case ExifDataType.Long: case ExifDataType.Long:
case ExifDataType.SignedLong: case ExifDataType.SignedLong:
case ExifDataType.SingleFloat: case ExifDataType.SingleFloat:
case ExifDataType.Ifd:
return 4; return 4;
case ExifDataType.DoubleFloat: case ExifDataType.DoubleFloat:
case ExifDataType.Rational: case ExifDataType.Rational:
case ExifDataType.SignedRational: case ExifDataType.SignedRational:
case ExifDataType.Long8:
case ExifDataType.SignedLong8:
case ExifDataType.Ifd8:
return 8; return 8;
default: default:
throw new NotSupportedException(dataType.ToString()); throw new NotSupportedException(dataType.ToString());

121
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<byte> AsciiCodeBytes => new byte[] { 0x41, 0x53, 0x43, 0x49, 0x49, 0, 0, 0 };
private static ReadOnlySpan<byte> JISCodeBytes => new byte[] { 0x4A, 0x49, 0x53, 0, 0, 0, 0, 0 };
private static ReadOnlySpan<byte> UnicodeCodeBytes => new byte[] { 0x55, 0x4E, 0x49, 0x43, 0x4F, 0x44, 0x45, 0 };
private static ReadOnlySpan<byte> 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<byte> 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<byte> 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<byte> 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<byte> destination)
{
fixed (char* c = value)
{
fixed (byte* b = destination)
{
return encoding.GetBytes(c, value.Length, b, destination.Length);
}
}
}
private static bool TryDetect(ReadOnlySpan<byte> 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;
}
}
}

298
src/ImageSharp/Metadata/Profiles/Exif/ExifReader.cs

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System; using System;
using System.Buffers;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
@ -9,15 +10,19 @@ using System.Diagnostics;
using System.IO; using System.IO;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text; using System.Text;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Metadata.Profiles.Exif namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
{ {
internal class ExifReader : BaseExifReader internal class ExifReader : BaseExifReader
{ {
private readonly List<Action> loaders = new List<Action>();
public ExifReader(byte[] exifData) 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); this.ReadSubIfd(values);
foreach (Action loader in this.loaders) this.ReadBigValues(values);
{
loader();
}
return values; return values;
} }
protected override void RegisterExtLoader(uint offset, Action loader) => this.loaders.Add(loader);
private void GetThumbnail(uint offset) private void GetThumbnail(uint offset)
{ {
if (offset == 0) if (offset == 0)
@ -86,19 +86,21 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
/// </summary> /// </summary>
internal abstract class BaseExifReader 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[] buf4 = new byte[4];
private readonly byte[] buf2 = new byte[2]; private readonly byte[] buf2 = new byte[2];
private readonly MemoryAllocator allocator;
private readonly Stream data; private readonly Stream data;
private List<ExifTag> invalidTags; private List<ExifTag> invalidTags;
private uint exifOffset; private List<ulong> subIfds;
private uint gpsOffset;
protected BaseExifReader(Stream stream) => protected BaseExifReader(Stream stream, MemoryAllocator allocator)
{
this.data = stream ?? throw new ArgumentNullException(nameof(stream)); this.data = stream ?? throw new ArgumentNullException(nameof(stream));
this.allocator = allocator;
}
private delegate TDataType ConverterMethod<TDataType>(ReadOnlySpan<byte> data); private delegate TDataType ConverterMethod<TDataType>(ReadOnlySpan<byte> data);
@ -119,7 +121,51 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
public bool IsBigEndian { get; protected set; } 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<IExifValue> 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<ulong>(size, int.MaxValue, nameof(size));
if ((int)size > maxSize)
{
maxSize = (int)size;
}
}
if (this.allocator != null)
{
// tiff, bigTiff
using IMemoryOwner<byte> memory = this.allocator.Allocate<byte>(maxSize);
Span<byte> 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<byte> 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();
}
/// <summary> /// <summary>
/// Reads the values to the values collection. /// Reads the values to the values collection.
@ -136,22 +182,45 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
this.Seek(offset); this.Seek(offset);
int count = this.ReadUInt16(); int count = this.ReadUInt16();
Span<byte> offsetBuffer = stackalloc byte[4];
for (int i = 0; i < count; i++) for (int i = 0; i < count; i++)
{ {
this.ReadValue(values); this.ReadValue(values, offsetBuffer);
} }
} }
protected void ReadSubIfd(List<IExifValue> values) protected void ReadSubIfd(List<IExifValue> 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<IExifValue> 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<byte> 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<IExifValue> values, (ulong Offset, ExifDataType DataType, ulong NumberOfComponents, ExifValue Exif) tag, Span<byte> 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; return result;
} }
private byte ConvertToByte(ReadOnlySpan<byte> buffer) => buffer[0]; private static string ConvertToString(Encoding encoding, ReadOnlySpan<byte> buffer)
private string ConvertToString(ReadOnlySpan<byte> buffer)
{ {
int nullCharIndex = buffer.IndexOf((byte)0); int nullCharIndex = buffer.IndexOf((byte)0);
@ -183,10 +250,12 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
buffer = buffer.Slice(0, nullCharIndex); buffer = buffer.Slice(0, nullCharIndex);
} }
return Encoding.UTF8.GetString(buffer); return encoding.GetString(buffer);
} }
private object ConvertValue(ExifDataType dataType, ReadOnlySpan<byte> buffer, uint numberOfComponents) private byte ConvertToByte(ReadOnlySpan<byte> buffer) => buffer[0];
private object ConvertValue(ExifDataType dataType, ReadOnlySpan<byte> buffer, bool isArray)
{ {
if (buffer.Length == 0) if (buffer.Length == 0)
{ {
@ -198,90 +267,101 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
case ExifDataType.Unknown: case ExifDataType.Unknown:
return null; return null;
case ExifDataType.Ascii: case ExifDataType.Ascii:
return this.ConvertToString(buffer); return ConvertToString(ExifConstants.DefaultEncoding, buffer);
case ExifDataType.Byte: case ExifDataType.Byte:
if (numberOfComponents == 1) case ExifDataType.Undefined:
if (!isArray)
{ {
return this.ConvertToByte(buffer); return this.ConvertToByte(buffer);
} }
return buffer.ToArray(); return buffer.ToArray();
case ExifDataType.DoubleFloat: case ExifDataType.DoubleFloat:
if (numberOfComponents == 1) if (!isArray)
{ {
return this.ConvertToDouble(buffer); return this.ConvertToDouble(buffer);
} }
return ToArray(dataType, buffer, this.ConvertToDouble); return ToArray(dataType, buffer, this.ConvertToDouble);
case ExifDataType.Long: case ExifDataType.Long:
if (numberOfComponents == 1) case ExifDataType.Ifd:
if (!isArray)
{ {
return this.ConvertToUInt32(buffer); return this.ConvertToUInt32(buffer);
} }
return ToArray(dataType, buffer, this.ConvertToUInt32); return ToArray(dataType, buffer, this.ConvertToUInt32);
case ExifDataType.Rational: case ExifDataType.Rational:
if (numberOfComponents == 1) if (!isArray)
{ {
return this.ToRational(buffer); return this.ToRational(buffer);
} }
return ToArray(dataType, buffer, this.ToRational); return ToArray(dataType, buffer, this.ToRational);
case ExifDataType.Short: case ExifDataType.Short:
if (numberOfComponents == 1) if (!isArray)
{ {
return this.ConvertToShort(buffer); return this.ConvertToShort(buffer);
} }
return ToArray(dataType, buffer, this.ConvertToShort); return ToArray(dataType, buffer, this.ConvertToShort);
case ExifDataType.SignedByte: case ExifDataType.SignedByte:
if (numberOfComponents == 1) if (!isArray)
{ {
return this.ConvertToSignedByte(buffer); return this.ConvertToSignedByte(buffer);
} }
return ToArray(dataType, buffer, this.ConvertToSignedByte); return ToArray(dataType, buffer, this.ConvertToSignedByte);
case ExifDataType.SignedLong: case ExifDataType.SignedLong:
if (numberOfComponents == 1) if (!isArray)
{ {
return this.ConvertToInt32(buffer); return this.ConvertToInt32(buffer);
} }
return ToArray(dataType, buffer, this.ConvertToInt32); return ToArray(dataType, buffer, this.ConvertToInt32);
case ExifDataType.SignedRational: case ExifDataType.SignedRational:
if (numberOfComponents == 1) if (!isArray)
{ {
return this.ToSignedRational(buffer); return this.ToSignedRational(buffer);
} }
return ToArray(dataType, buffer, this.ToSignedRational); return ToArray(dataType, buffer, this.ToSignedRational);
case ExifDataType.SignedShort: case ExifDataType.SignedShort:
if (numberOfComponents == 1) if (!isArray)
{ {
return this.ConvertToSignedShort(buffer); return this.ConvertToSignedShort(buffer);
} }
return ToArray(dataType, buffer, this.ConvertToSignedShort); return ToArray(dataType, buffer, this.ConvertToSignedShort);
case ExifDataType.SingleFloat: case ExifDataType.SingleFloat:
if (numberOfComponents == 1) if (!isArray)
{ {
return this.ConvertToSingle(buffer); return this.ConvertToSingle(buffer);
} }
return ToArray(dataType, buffer, this.ConvertToSingle); return ToArray(dataType, buffer, this.ConvertToSingle);
case ExifDataType.Undefined: case ExifDataType.Long8:
if (numberOfComponents == 1) 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: default:
throw new NotSupportedException(); throw new NotSupportedException($"Data type {dataType} is not supported.");
} }
} }
private void ReadValue(List<IExifValue> values) private void ReadValue(List<IExifValue> values, Span<byte> offsetBuffer)
{ {
// 2 | 2 | 4 | 4 // 2 | 2 | 4 | 4
// tag | type | count | value offset // tag | type | count | value offset
@ -295,7 +375,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
uint numberOfComponents = this.ReadUInt32(); uint numberOfComponents = this.ReadUInt32();
this.TryReadSpan(this.offsetBuffer); this.TryReadSpan(offsetBuffer);
// Ensure that the data type is valid // Ensure that the data type is valid
if (dataType == ExifDataType.Unknown) if (dataType == ExifDataType.Unknown)
@ -305,9 +385,9 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
// Issue #132: ExifDataType == Undefined is treated like a byte array. // 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 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); 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); uint size = numberOfComponents * ExifDataTypes.GetSize(dataType);
if (size > 4) if (size > 4)
{ {
uint newOffset = this.ConvertToUInt32(this.offsetBuffer); uint newOffset = this.ConvertToUInt32(offsetBuffer);
// Ensure that the new index does not overrun the data. // Ensure that the new index does not overrun the data.
if (newOffset > int.MaxValue || (newOffset + size) > this.data.Length) if (newOffset > int.MaxValue || (newOffset + size) > this.data.Length)
@ -330,21 +410,85 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
return; return;
} }
this.RegisterExtLoader(newOffset, () => this.BigValues.Add((newOffset, dataType, numberOfComponents, exifValue));
{
byte[] dataBuffer = new byte[size];
this.Seek(newOffset);
if (this.TryReadSpan(dataBuffer))
{
object value = this.ConvertValue(dataType, dataBuffer, numberOfComponents);
this.Add(values, exifValue, value);
}
});
} }
else 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<IExifValue> values, Span<byte> 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); this.Add(values, exifValue, value);
} }
} }
@ -368,11 +512,11 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
if (exif.Tag == ExifTag.SubIFDOffset) if (exif.Tag == ExifTag.SubIFDOffset)
{ {
this.exifOffset = (uint)value; this.AddSubIfd(value);
} }
else if (exif.Tag == ExifTag.GPSIFDOffset) else if (exif.Tag == ExifTag.GPSIFDOffset)
{ {
this.gpsOffset = (uint)value; this.AddSubIfd(value);
} }
else else
{ {
@ -383,8 +527,11 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
private void AddInvalidTag(ExifTag tag) private void AddInvalidTag(ExifTag tag)
=> (this.invalidTags ??= new List<ExifTag>()).Add(tag); => (this.invalidTags ??= new List<ExifTag>()).Add(tag);
private void Seek(long pos) private void AddSubIfd(object val)
=> this.data.Seek(pos, SeekOrigin.Begin); => (this.subIfds ??= new List<ulong>()).Add(Convert.ToUInt64(val));
private void Seek(ulong pos)
=> this.data.Seek((long)pos, SeekOrigin.Begin);
private bool TryReadSpan(Span<byte> span) private bool TryReadSpan(Span<byte> span)
{ {
@ -398,6 +545,11 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
return read == length; return read == length;
} }
protected ulong ReadUInt64() =>
this.TryReadSpan(this.buf8)
? this.ConvertToUInt64(this.buf8)
: default;
// Known as Long in Exif Specification. // Known as Long in Exif Specification.
protected uint ReadUInt32() => protected uint ReadUInt32() =>
this.TryReadSpan(this.buf4) this.TryReadSpan(this.buf4)
@ -408,6 +560,30 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
? this.ConvertToShort(this.buf2) ? this.ConvertToShort(this.buf2)
: default; : default;
private long ConvertToInt64(ReadOnlySpan<byte> buffer)
{
if (buffer.Length < 8)
{
return default;
}
return this.IsBigEndian
? BinaryPrimitives.ReadInt64BigEndian(buffer)
: BinaryPrimitives.ReadInt64LittleEndian(buffer);
}
private ulong ConvertToUInt64(ReadOnlySpan<byte> buffer)
{
if (buffer.Length < 8)
{
return default;
}
return this.IsBigEndian
? BinaryPrimitives.ReadUInt64BigEndian(buffer)
: BinaryPrimitives.ReadUInt64LittleEndian(buffer);
}
private double ConvertToDouble(ReadOnlySpan<byte> buffer) private double ConvertToDouble(ReadOnlySpan<byte> buffer)
{ {
if (buffer.Length < 8) if (buffer.Length < 8)

21
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<byte> destination) => ExifEncodedStringHelpers.Write(Ucs2Encoding, value, destination);
}
}

88
src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs

@ -4,7 +4,6 @@
using System; using System;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text;
namespace SixLabors.ImageSharp.Metadata.Profiles.Exif namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
{ {
@ -46,23 +45,17 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
public byte[] GetData() public byte[] GetData()
{ {
const uint startIndex = 0; const uint startIndex = 0;
uint length;
IExifValue exifOffset = GetOffsetValue(this.ifdValues, this.exifValues, ExifTag.SubIFDOffset); IExifValue exifOffset = GetOffsetValue(this.ifdValues, this.exifValues, ExifTag.SubIFDOffset);
IExifValue gpsOffset = GetOffsetValue(this.ifdValues, this.gpsValues, ExifTag.GPSIFDOffset); IExifValue gpsOffset = GetOffsetValue(this.ifdValues, this.gpsValues, ExifTag.GPSIFDOffset);
if (this.ifdValues.Count == 0 && this.exifValues.Count == 0 && this.gpsValues.Count == 0) uint ifdLength = this.GetLength(this.ifdValues);
{
return Array.Empty<byte>();
}
uint ifdLength = this.GetLength(this.ifdValues) + 4U;
uint exifLength = this.GetLength(this.exifValues); uint exifLength = this.GetLength(this.exifValues);
uint gpsLength = this.GetLength(this.gpsValues); uint gpsLength = this.GetLength(this.gpsValues);
length = ifdLength + exifLength + gpsLength; uint length = ifdLength + exifLength + gpsLength;
if (length == 4U) if (length == 0)
{ {
return Array.Empty<byte>(); return Array.Empty<byte>();
} }
@ -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 // 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 += (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; int i = 0;
@ -80,15 +74,13 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
ExifConstants.LittleEndianByteOrderMarker.CopyTo(result.AsSpan(start: i)); ExifConstants.LittleEndianByteOrderMarker.CopyTo(result.AsSpan(start: i));
i += ExifConstants.LittleEndianByteOrderMarker.Length; i += ExifConstants.LittleEndianByteOrderMarker.Length;
uint ifdOffset = ((uint)i - startIndex) + 4U; uint ifdOffset = (uint)i - startIndex + 4U;
uint thumbnailOffset = ifdOffset + ifdLength + exifLength + gpsLength;
exifOffset?.TrySetValue(ifdOffset + ifdLength); exifOffset?.TrySetValue(ifdOffset + ifdLength);
gpsOffset?.TrySetValue(ifdOffset + ifdLength + exifLength); gpsOffset?.TrySetValue(ifdOffset + ifdLength + exifLength);
i = WriteUInt32(ifdOffset, result, i); i = WriteUInt32(ifdOffset, result, i);
i = this.WriteHeaders(this.ifdValues, result, i); i = this.WriteHeaders(this.ifdValues, result, i);
i = WriteUInt32(thumbnailOffset, result, i);
i = this.WriteData(startIndex, this.ifdValues, result, i); i = this.WriteData(startIndex, this.ifdValues, result, i);
if (exifLength > 0) if (exifLength > 0)
@ -103,8 +95,6 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
i = this.WriteData(startIndex, this.gpsValues, result, i); i = this.WriteData(startIndex, this.gpsValues, result, i);
} }
WriteUInt16(0, result, i);
return result; return result;
} }
@ -150,6 +140,20 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
return offset + 4; return offset + 4;
} }
private static int WriteInt64(long value, Span<byte> destination, int offset)
{
BinaryPrimitives.WriteInt64LittleEndian(destination.Slice(offset, 8), value);
return offset + 8;
}
private static int WriteUInt64(ulong value, Span<byte> destination, int offset)
{
BinaryPrimitives.WriteUInt64LittleEndian(destination.Slice(offset, 8), value);
return offset + 8;
}
private static int WriteInt32(int value, Span<byte> destination, int offset) private static int WriteInt32(int value, Span<byte> destination, int offset)
{ {
BinaryPrimitives.WriteInt32LittleEndian(destination.Slice(offset, 4), value); BinaryPrimitives.WriteInt32LittleEndian(destination.Slice(offset, 4), value);
@ -249,7 +253,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
{ {
uint valueLength = GetLength(value); uint valueLength = GetLength(value);
length += 2 + 2 + 4 + 4; length += 12;
if (valueLength > 4) if (valueLength > 4)
{ {
@ -257,6 +261,9 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
} }
} }
// next IFD offset
length += 4;
return length; return length;
} }
@ -266,9 +273,19 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
{ {
object value = exifValue.GetValue(); 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) 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) if (value is Array arrayValue)
@ -281,11 +298,6 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
private static int WriteArray(IExifValue value, Span<byte> destination, int offset) private static int WriteArray(IExifValue value, Span<byte> destination, int offset)
{ {
if (value.DataType == ExifDataType.Ascii)
{
return WriteValue(ExifDataType.Ascii, value.GetValue(), destination, offset);
}
int newOffset = offset; int newOffset = offset;
foreach (object obj in (Array)value.GetValue()) foreach (object obj in (Array)value.GetValue())
{ {
@ -347,6 +359,9 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
newOffset += 4; newOffset += 4;
} }
// next IFD offset
newOffset = WriteUInt32(0, destination, newOffset);
return newOffset; return newOffset;
} }
@ -367,7 +382,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
switch (dataType) switch (dataType)
{ {
case ExifDataType.Ascii: case ExifDataType.Ascii:
offset = Write(Encoding.UTF8.GetBytes((string)value), destination, offset); offset = Write(ExifConstants.DefaultEncoding.GetBytes((string)value), destination, offset);
destination[offset] = 0; destination[offset] = 0;
return offset + 1; return offset + 1;
case ExifDataType.Byte: case ExifDataType.Byte:
@ -390,6 +405,10 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
} }
return WriteUInt32((uint)value, destination, offset); 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: case ExifDataType.Rational:
WriteRational(destination.Slice(offset, 8), (Rational)value); WriteRational(destination.Slice(offset, 8), (Rational)value);
return offset + 8; return offset + 8;
@ -410,14 +429,25 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
} }
} }
internal static int WriteValue(IExifValue value, Span<byte> destination, int offset) internal static int WriteValue(IExifValue exifValue, Span<byte> 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);
} }
} }
} }

25
src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.ByteArray.cs

@ -41,31 +41,6 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
/// </summary> /// </summary>
public static ExifTag<byte[]> TIFFEPStandardID => new ExifTag<byte[]>(ExifTagValue.TIFFEPStandardID); public static ExifTag<byte[]> TIFFEPStandardID => new ExifTag<byte[]>(ExifTagValue.TIFFEPStandardID);
/// <summary>
/// Gets the XPTitle exif tag.
/// </summary>
public static ExifTag<byte[]> XPTitle => new ExifTag<byte[]>(ExifTagValue.XPTitle);
/// <summary>
/// Gets the XPComment exif tag.
/// </summary>
public static ExifTag<byte[]> XPComment => new ExifTag<byte[]>(ExifTagValue.XPComment);
/// <summary>
/// Gets the XPAuthor exif tag.
/// </summary>
public static ExifTag<byte[]> XPAuthor => new ExifTag<byte[]>(ExifTagValue.XPAuthor);
/// <summary>
/// Gets the XPKeywords exif tag.
/// </summary>
public static ExifTag<byte[]> XPKeywords => new ExifTag<byte[]>(ExifTagValue.XPKeywords);
/// <summary>
/// Gets the XPSubject exif tag.
/// </summary>
public static ExifTag<byte[]> XPSubject => new ExifTag<byte[]>(ExifTagValue.XPSubject);
/// <summary> /// <summary>
/// Gets the GPSVersionID exif tag. /// Gets the GPSVersionID exif tag.
/// </summary> /// </summary>

24
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
{
/// <content/>
public abstract partial class ExifTag
{
/// <summary>
/// Gets the UserComment exif tag.
/// </summary>
public static ExifTag<EncodedString> UserComment { get; } = new ExifTag<EncodedString>(ExifTagValue.UserComment);
/// <summary>
/// Gets the GPSProcessingMethod exif tag.
/// </summary>
public static ExifTag<EncodedString> GPSProcessingMethod { get; } = new ExifTag<EncodedString>(ExifTagValue.GPSProcessingMethod);
/// <summary>
/// Gets the GPSAreaInformation exif tag.
/// </summary>
public static ExifTag<EncodedString> GPSAreaInformation { get; } = new ExifTag<EncodedString>(ExifTagValue.GPSAreaInformation);
}
}

5
src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs

@ -65,5 +65,10 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
/// Gets the TimeZoneOffset exif tag. /// Gets the TimeZoneOffset exif tag.
/// </summary> /// </summary>
public static ExifTag<uint[]> TimeZoneOffset { get; } = new ExifTag<uint[]>(ExifTagValue.TimeZoneOffset); public static ExifTag<uint[]> TimeZoneOffset { get; } = new ExifTag<uint[]>(ExifTagValue.TimeZoneOffset);
/// <summary>
/// Gets the offset to child IFDs exif tag.
/// </summary>
public static ExifTag<uint[]> SubIFDs { get; } = new ExifTag<uint[]>(ExifTagValue.SubIFDs);
} }
} }

34
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
{
/// <content/>
public abstract partial class ExifTag
{
/// <summary>
/// Gets the title tag used by Windows (encoded in UCS2).
/// </summary>
public static ExifTag<string> XPTitle => new ExifTag<string>(ExifTagValue.XPTitle);
/// <summary>
/// Gets the comment tag used by Windows (encoded in UCS2).
/// </summary>
public static ExifTag<string> XPComment => new ExifTag<string>(ExifTagValue.XPComment);
/// <summary>
/// Gets the author tag used by Windows (encoded in UCS2).
/// </summary>
public static ExifTag<string> XPAuthor => new ExifTag<string>(ExifTagValue.XPAuthor);
/// <summary>
/// Gets the keywords tag used by Windows (encoded in UCS2).
/// </summary>
public static ExifTag<string> XPKeywords => new ExifTag<string>(ExifTagValue.XPKeywords);
/// <summary>
/// Gets the subject tag used by Windows (encoded in UCS2).
/// </summary>
public static ExifTag<string> XPSubject => new ExifTag<string>(ExifTagValue.XPSubject);
}
}

15
src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Undefined.cs

@ -31,11 +31,6 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
/// </summary> /// </summary>
public static ExifTag<byte[]> MakerNote { get; } = new ExifTag<byte[]>(ExifTagValue.MakerNote); public static ExifTag<byte[]> MakerNote { get; } = new ExifTag<byte[]>(ExifTagValue.MakerNote);
/// <summary>
/// Gets the UserComment exif tag.
/// </summary>
public static ExifTag<byte[]> UserComment { get; } = new ExifTag<byte[]>(ExifTagValue.UserComment);
/// <summary> /// <summary>
/// Gets the FlashpixVersion exif tag. /// Gets the FlashpixVersion exif tag.
/// </summary> /// </summary>
@ -71,16 +66,6 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif
/// </summary> /// </summary>
public static ExifTag<byte[]> ImageSourceData { get; } = new ExifTag<byte[]>(ExifTagValue.ImageSourceData); public static ExifTag<byte[]> ImageSourceData { get; } = new ExifTag<byte[]>(ExifTagValue.ImageSourceData);
/// <summary>
/// Gets the GPSProcessingMethod exif tag.
/// </summary>
public static ExifTag<byte[]> GPSProcessingMethod { get; } = new ExifTag<byte[]>(ExifTagValue.GPSProcessingMethod);
/// <summary>
/// Gets the GPSAreaInformation exif tag.
/// </summary>
public static ExifTag<byte[]> GPSAreaInformation { get; } = new ExifTag<byte[]>(ExifTagValue.GPSAreaInformation);
/// <summary> /// <summary>
/// Gets the FileSource exif tag. /// Gets the FileSource exif tag.
/// </summary> /// </summary>

94
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
{
/// <summary>
/// The EXIF encoded string structure.
/// </summary>
public readonly struct EncodedString : IEquatable<EncodedString>
{
/// <summary>
/// Initializes a new instance of the <see cref="EncodedString" /> struct.
/// Default use Unicode character code.
/// </summary>
/// <param name="text">The text value.</param>
public EncodedString(string text)
: this(CharacterCode.Unicode, text)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="EncodedString" /> struct.
/// </summary>
/// <param name="code">The character code.</param>
/// <param name="text">The text value.</param>
public EncodedString(CharacterCode code, string text)
{
this.Text = text;
this.Code = code;
}
/// <summary>
/// The 8-byte character code enum.
/// </summary>
public enum CharacterCode
{
/// <summary>
/// The ASCII (ITU-T T.50 IA5) character code.
/// </summary>
ASCII,
/// <summary>
/// The JIS (X208-1990) character code.
/// </summary>
JIS,
/// <summary>
/// The Unicode character code.
/// </summary>
Unicode,
/// <summary>
/// The undefined character code.
/// </summary>
Undefined
}
/// <summary>
/// Gets the character ode.
/// </summary>
public CharacterCode Code { get; }
/// <summary>
/// Gets the text.
/// </summary>
public string Text { get; }
/// <summary>
/// Converts the specified <see cref="string"/> to an instance of this type.
/// </summary>
/// <param name="text">The text value.</param>
public static implicit operator EncodedString(string text) => new(text);
/// <summary>
/// Converts the specified <see cref="EncodedString"/> to a <see cref="string"/>.
/// </summary>
/// <param name="encodedString">The <see cref="EncodedString"/> to convert.</param>
public static explicit operator string(EncodedString encodedString) => encodedString.Text;
/// <inheritdoc/>
public override bool Equals(object obj) => obj is EncodedString other && this.Equals(other);
/// <inheritdoc/>
public bool Equals(EncodedString other) => this.Text == other.Text && this.Code == other.Code;
/// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(this.Text, this.Code);
/// <inheritdoc/>
public override string ToString() => this.Text;
}
}

55
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<EncodedString>
{
public ExifEncodedString(ExifTag<EncodedString> 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);
}
}

66
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<ulong>
{
public ExifLong8(ExifTag<ulong> 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);
}
}

171
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<ulong>
{
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;
}
}
}

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

Loading…
Cancel
Save